diff --git a/app/build.gradle b/app/build.gradle
index 9e40c569..b313c862 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -30,7 +30,6 @@ android {
}
buildFeatures {
- dataBinding true
viewBinding = true
}
}
@@ -58,6 +57,8 @@ dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
+ implementation 'androidx.palette:palette:1.0.0'
+
// Android Material
implementation 'com.google.android.material:material:1.2.1'
@@ -74,10 +75,11 @@ dependencies {
// Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
+ implementation 'com.github.bumptech.glide:okhttp3-integration:4.11.0'
implementation "com.github.woltapp:blurhash:f41a23cc50"
// Exoplayer
- implementation 'com.google.android.exoplayer:exoplayer:2.12.2'
+ implementation 'com.google.android.exoplayer:exoplayer:2.11.4'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
annotationProcessor "androidx.room:room-compiler:2.2.5"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ba99cfae..531efac3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,9 +3,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.cappielloantonio.play">
-
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/glide/palette/BitmapPaletteWrapper.java b/app/src/main/java/com/cappielloantonio/play/glide/palette/BitmapPaletteWrapper.java
new file mode 100644
index 00000000..e6817671
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/glide/palette/BitmapPaletteWrapper.java
@@ -0,0 +1,23 @@
+package com.cappielloantonio.play.glide.palette;
+
+import android.graphics.Bitmap;
+
+import androidx.palette.graphics.Palette;
+
+public class BitmapPaletteWrapper {
+ private final Bitmap bitmap;
+ private final Palette palette;
+
+ public BitmapPaletteWrapper(Bitmap bitmap, Palette palette) {
+ this.bitmap = bitmap;
+ this.palette = palette;
+ }
+
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+
+ public Palette getPalette() {
+ return palette;
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/service/MediaButtonIntentReceiver.java b/app/src/main/java/com/cappielloantonio/play/service/MediaButtonIntentReceiver.java
new file mode 100644
index 00000000..50f610de
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/service/MediaButtonIntentReceiver.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+// Modified for Phonograph by Karim Abou Zeid (kabouzeid).
+
+package com.cappielloantonio.play.service;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import androidx.core.content.ContextCompat;
+
+import com.cappielloantonio.play.BuildConfig;
+
+/**
+ * Used to control headset playback.
+ * Single press: pause/resume
+ * Double press: next track
+ * Triple press: previous track
+ */
+public class MediaButtonIntentReceiver extends BroadcastReceiver {
+ private static final boolean DEBUG = BuildConfig.DEBUG;
+ public static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
+
+ private static final int MSG_HEADSET_DOUBLE_CLICK_TIMEOUT = 2;
+
+ private static final int DOUBLE_CLICK = 400;
+
+ private static WakeLock mWakeLock = null;
+ private static int mClickCounter = 0;
+ private static long mLastClickTime = 0;
+
+ @SuppressLint("HandlerLeak")
+ private static Handler mHandler = new Handler() {
+
+ @Override
+ public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case MSG_HEADSET_DOUBLE_CLICK_TIMEOUT:
+ final int clickCount = msg.arg1;
+ final String command;
+
+ if (DEBUG) Log.v(TAG, "Handling headset click, count = " + clickCount);
+ switch (clickCount) {
+ case 1:
+ command = MusicService.ACTION_TOGGLE;
+ break;
+ case 2:
+ command = MusicService.ACTION_SKIP;
+ break;
+ case 3:
+ command = MusicService.ACTION_REWIND;
+ break;
+ default:
+ command = null;
+ break;
+ }
+
+ if (command != null) {
+ final Context context = (Context) msg.obj;
+ startService(context, command);
+ }
+
+ break;
+ }
+
+ releaseWakeLockIfHandlerIdle();
+ }
+ };
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ if (DEBUG) Log.v(TAG, "Received intent: " + intent);
+ if (handleIntent(context, intent) && isOrderedBroadcast()) {
+ abortBroadcast();
+ }
+ }
+
+ public static boolean handleIntent(final Context context, final Intent intent) {
+ final String intentAction = intent.getAction();
+ if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
+ final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ if (event == null) {
+ return false;
+ }
+
+ final int keycode = event.getKeyCode();
+ final int action = event.getAction();
+
+ // fallback to system time if event time is not available
+ final long eventTime = event.getEventTime() != 0
+ ? event.getEventTime()
+ : System.currentTimeMillis();
+
+ String command = null;
+ switch (keycode) {
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ command = MusicService.ACTION_STOP;
+ break;
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ command = MusicService.ACTION_TOGGLE;
+ break;
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ command = MusicService.ACTION_SKIP;
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ command = MusicService.ACTION_REWIND;
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ command = MusicService.ACTION_PAUSE;
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ command = MusicService.ACTION_PLAY;
+ break;
+ }
+
+ if (command != null) {
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (event.getRepeatCount() == 0) {
+ // Only consider the first event in a sequence, not the repeat events,
+ // so that we don't trigger in cases where the first event went to
+ // a different app (e.g. when the user ends a phone call by
+ // long pressing the headset button)
+
+ // The service may or may not be running, but we need to send it
+ // a command.
+ if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+ if (eventTime - mLastClickTime >= DOUBLE_CLICK) {
+ mClickCounter = 0;
+ }
+
+ mClickCounter++;
+ if (DEBUG) Log.v(TAG, "Got headset click, count = " + mClickCounter);
+ mHandler.removeMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT);
+
+ Message msg = mHandler.obtainMessage(
+ MSG_HEADSET_DOUBLE_CLICK_TIMEOUT, mClickCounter, 0, context);
+
+ long delay = mClickCounter < 3 ? DOUBLE_CLICK : 0;
+ if (mClickCounter >= 3) {
+ mClickCounter = 0;
+ }
+
+ mLastClickTime = eventTime;
+ acquireWakeLockAndSendMessage(context, msg, delay);
+ } else {
+ startService(context, command);
+ }
+
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static void startService(Context context, String command) {
+ final Intent intent = new Intent(context, MusicService.class);
+ intent.setAction(command);
+ try {
+ // IMPORTANT NOTE: (kind of a hack)
+ // on Android O and above the following crashes when the app is not running
+ // there is no good way to check whether the app is running so we catch the exception
+ // we do not always want to use startForegroundService() because then one gets an ANR
+ // if no notification is displayed via startForeground()
+ // according to Play analytics this happens a lot, I suppose for example if command = PAUSE
+ context.startService(intent);
+ } catch (IllegalStateException ignored) {
+ ContextCompat.startForegroundService(context, intent);
+ }
+ }
+
+ private static void acquireWakeLockAndSendMessage(Context context, Message msg, long delay) {
+ if (mWakeLock == null) {
+ Context appContext = context.getApplicationContext();
+ PowerManager pm = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, context.getClass().getName());
+ mWakeLock.setReferenceCounted(false);
+ }
+
+ if (DEBUG) Log.v(TAG, "Acquiring wake lock and sending " + msg.what);
+
+ mWakeLock.acquire(10000);
+ mHandler.sendMessageDelayed(msg, delay);
+ }
+
+ private static void releaseWakeLockIfHandlerIdle() {
+ if (mHandler.hasMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)) {
+ if (DEBUG) Log.v(TAG, "Handler still has messages pending, not releasing wake lock");
+ return;
+ }
+
+ if (mWakeLock != null) {
+ if (DEBUG) Log.v(TAG, "Releasing wake lock");
+
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/service/MultiPlayer.java b/app/src/main/java/com/cappielloantonio/play/service/MultiPlayer.java
new file mode 100644
index 00000000..3e20b29a
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/service/MultiPlayer.java
@@ -0,0 +1,290 @@
+package com.cappielloantonio.play.service;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.model.Song;
+import com.cappielloantonio.play.service.playback.Playback;
+import com.cappielloantonio.play.util.MusicUtil;
+import com.cappielloantonio.play.util.PreferenceUtil;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.database.ExoDatabaseProvider;
+import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.FileDataSource;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
+import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
+import com.google.android.exoplayer2.upstream.cache.SimpleCache;
+
+import java.io.File;
+import java.io.IOException;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.Dispatcher;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class MultiPlayer implements Playback {
+ public static final String TAG = MultiPlayer.class.getSimpleName();
+
+ private final Context context;
+ private final OkHttpClient httpClient;
+
+ private SimpleExoPlayer exoPlayer;
+ private ConcatenatingMediaSource mediaSource;
+
+ private final SimpleCache simpleCache;
+ private final DataSource.Factory dataSource;
+
+ private PlaybackCallbacks callbacks;
+
+ private boolean isReady = false;
+ private boolean isPlaying = false;
+
+ private boolean requestPlay = false;
+ private int requestProgress = 0;
+
+ private final ExoPlayer.EventListener eventListener = new ExoPlayer.EventListener() {
+ @Override
+ public void onTracksChanged(@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) {
+ Log.i(TAG, "onTracksChanged");
+ }
+
+ @Override
+ public void onLoadingChanged(boolean isLoading) {
+ Log.i(TAG, "onLoadingChanged: " + isLoading);
+ }
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ Log.i(TAG, "onPlayerStateChanged playWhenReady: " + playWhenReady);
+ Log.i(TAG, "onPlayerStateChanged playbackState: " + playbackState);
+
+ if (callbacks == null) return;
+ if (requestProgress != 0 && playbackState == Player.STATE_READY) {
+ exoPlayer.seekTo(requestProgress);
+
+ requestProgress = 0;
+ }
+
+ if (exoPlayer.isPlaying() || requestPlay && playbackState == ExoPlayer.STATE_READY) {
+ requestPlay = false;
+ isPlaying = true;
+
+ exoPlayer.setPlayWhenReady(true);
+ callbacks.onTrackStarted();
+ }
+ }
+
+ @Override
+ public void onPositionDiscontinuity(int reason) {
+ Log.i(TAG, "onPositionDiscontinuity: " + reason);
+ int windowIndex = exoPlayer.getCurrentWindowIndex();
+
+ if (windowIndex == 1) {
+ mediaSource.removeMediaSource(0);
+ if (exoPlayer.isPlaying()) {
+ // there are still songs left in the queue
+ callbacks.onTrackWentToNext();
+ } else {
+ callbacks.onTrackEnded();
+ }
+ }
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException error) {
+ Log.i(TAG, "onPlayerError: " + error.getMessage());
+ if (context == null) {
+ return;
+ }
+
+ Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
+ exoPlayer.release();
+
+ exoPlayer = new SimpleExoPlayer.Builder(context).build();
+ isReady = false;
+ }
+ };
+
+ public MultiPlayer(Context context) {
+ this.context = context;
+
+ Dispatcher dispatcher = new Dispatcher();
+ dispatcher.setMaxRequests(1);
+
+ httpClient = new OkHttpClient.Builder().dispatcher(dispatcher).build();
+
+ exoPlayer = new SimpleExoPlayer.Builder(context).build();
+ mediaSource = new ConcatenatingMediaSource();
+
+ exoPlayer.addListener(eventListener);
+ exoPlayer.prepare(mediaSource);
+ exoPlayer.setRepeatMode(Player.REPEAT_MODE_OFF);
+
+ long cacheSize = PreferenceUtil.getInstance(context).getMediaCacheSize();
+ LeastRecentlyUsedCacheEvictor recentlyUsedCache = new LeastRecentlyUsedCacheEvictor(cacheSize);
+ ExoDatabaseProvider databaseProvider = new ExoDatabaseProvider(context);
+
+ File cacheDirectory = new File(context.getCacheDir(), "exoplayer");
+ simpleCache = new SimpleCache(cacheDirectory, recentlyUsedCache, databaseProvider);
+ dataSource = buildDataSourceFactory();
+ }
+
+ @Override
+ public void setDataSource(Song song) {
+ isReady = false;
+ mediaSource = new ConcatenatingMediaSource();
+
+ exoPlayer.addListener(eventListener);
+ exoPlayer.prepare(mediaSource);
+
+ // queue and other information is currently handled outside exoplayer
+ exoPlayer.setRepeatMode(Player.REPEAT_MODE_OFF);
+
+ appendDataSource(MusicUtil.getSongFileUri(song));
+ }
+
+ @Override
+ public void queueDataSource(Song song) {
+ String path = MusicUtil.getSongFileUri(song);
+ if (mediaSource.getSize() == 2 && mediaSource.getMediaSource(1).getTag() != path) {
+ mediaSource.removeMediaSource(1);
+ }
+
+ if (mediaSource.getSize() != 2) {
+ appendDataSource(path);
+ }
+ }
+
+ private void appendDataSource(String path) {
+ Uri uri = Uri.parse(path);
+
+ httpClient.newCall(new Request.Builder().url(path).head().build()).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
+ e.printStackTrace();
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ MediaSource source;
+ if (response.header("Content-Type").equals("application/x-mpegURL")) {
+ source = new HlsMediaSource.Factory(dataSource)
+ .setTag(path)
+ .setAllowChunklessPreparation(true)
+ .createMediaSource(uri);
+ } else {
+ source = new ProgressiveMediaSource.Factory(dataSource)
+ .setTag(path)
+ .createMediaSource(uri);
+ }
+
+ mediaSource.addMediaSource(source);
+ isReady = true;
+ }
+ });
+ }
+
+ private DataSource.Factory buildDataSourceFactory() {
+ return () -> new CacheDataSource(
+ simpleCache,
+ new DefaultDataSourceFactory(context, context.getPackageName(), null).createDataSource(),
+ new FileDataSource(),
+ new CacheDataSink(simpleCache, 10 * 1024 * 1024),
+ CacheDataSource.FLAG_BLOCK_ON_CACHE,
+ null
+ );
+ }
+
+ @Override
+ public void setCallbacks(Playback.PlaybackCallbacks callbacks) {
+ this.callbacks = callbacks;
+ }
+
+ @Override
+ public boolean isReady() {
+ return isReady;
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return isReady && isPlaying;
+ }
+
+ @Override
+ public void start() {
+ if (!isReady) {
+ requestPlay = true;
+ return;
+ }
+
+ isPlaying = true;
+ exoPlayer.setPlayWhenReady(true);
+ }
+
+ @Override
+ public void pause() {
+ isPlaying = false;
+ exoPlayer.setPlayWhenReady(false);
+ }
+
+ @Override
+ public void stop() {
+ simpleCache.release();
+ exoPlayer.release();
+
+ exoPlayer = null;
+ isReady = false;
+ }
+
+ @Override
+ public int getProgress() {
+ if (!isReady) return -1;
+ return (int) exoPlayer.getCurrentPosition();
+ }
+
+ @Override
+ public int getDuration() {
+ if (!isReady) return -1;
+ return (int) exoPlayer.getDuration();
+ }
+
+ @Override
+ public void setProgress(int progress) {
+ if (!isReady) {
+ requestProgress = progress;
+ return;
+ }
+
+ exoPlayer.seekTo(progress);
+ }
+
+ @Override
+ public void setVolume(int volume) {
+ exoPlayer.setVolume(volume / 100f);
+ }
+
+ @Override
+ public int getVolume() {
+ return (int) (exoPlayer.getVolume() * 100);
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/service/MusicService.java b/app/src/main/java/com/cappielloantonio/play/service/MusicService.java
index 15e3434f..34c978e7 100644
--- a/app/src/main/java/com/cappielloantonio/play/service/MusicService.java
+++ b/app/src/main/java/com/cappielloantonio/play/service/MusicService.java
@@ -1,97 +1,1100 @@
package com.cappielloantonio.play.service;
-import android.app.Notification;
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.media.AudioManager;
-import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.Process;
+import android.preference.PreferenceManager;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
+import com.cappielloantonio.play.App;
import com.cappielloantonio.play.R;
-import com.google.android.exoplayer2.ExoPlayerFactory;
-import com.google.android.exoplayer2.SimpleExoPlayer;
-import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.ProgressiveMediaSource;
-import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
-import com.google.android.exoplayer2.util.Util;
+import com.cappielloantonio.play.model.Playlist;
+import com.cappielloantonio.play.model.Song;
+import com.cappielloantonio.play.service.notification.PlayingNotification;
+import com.cappielloantonio.play.service.playback.Playback;
+import com.cappielloantonio.play.util.PreferenceUtil;
-public class MusicService extends Service implements AudioManager.OnAudioFocusChangeListener {
+import org.jellyfin.apiclient.interaction.EmptyResponse;
+import org.jellyfin.apiclient.interaction.Response;
+import org.jellyfin.apiclient.model.session.PlaybackProgressInfo;
+import org.jellyfin.apiclient.model.session.PlaybackStartInfo;
+import org.jellyfin.apiclient.model.session.PlaybackStopInfo;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class MusicService extends Service implements Playback.PlaybackCallbacks {
+ public static final String PACKAGE_NAME = "com.dkanada.gramophone";
+
+ public static final String ACTION_TOGGLE = PACKAGE_NAME + ".toggle";
+ public static final String ACTION_PLAY = PACKAGE_NAME + ".play";
+ public static final String ACTION_PLAY_PLAYLIST = PACKAGE_NAME + ".play.playlist";
+ public static final String ACTION_PAUSE = PACKAGE_NAME + ".pause";
+ public static final String ACTION_STOP = PACKAGE_NAME + ".stop";
+ public static final String ACTION_SKIP = PACKAGE_NAME + ".skip";
+ public static final String ACTION_REWIND = PACKAGE_NAME + ".rewind";
+ public static final String ACTION_QUIT = PACKAGE_NAME + ".quit";
+ public static final String ACTION_PENDING_QUIT = PACKAGE_NAME + ".quit.pending";
+
+ public static final String INTENT_EXTRA_PLAYLIST = PACKAGE_NAME + ".extra.playlist";
+ public static final String INTENT_EXTRA_SHUFFLE = PACKAGE_NAME + ".extra.shuffle";
+
+ public static final String STATE_CHANGED = PACKAGE_NAME + ".state.changed";
+ public static final String META_CHANGED = PACKAGE_NAME + ".meta.changed";
+ public static final String QUEUE_CHANGED = PACKAGE_NAME + ".queue.changed";
+
+ public static final String REPEAT_MODE_CHANGED = PACKAGE_NAME + ".repeat.changed";
+ public static final String SHUFFLE_MODE_CHANGED = PACKAGE_NAME + ".shuffle.changed";
+
+ public static final int TRACK_STARTED = 9;
+ public static final int TRACK_CHANGED = 1;
+ public static final int TRACK_ENDED = 2;
+
+ public static final int RELEASE_WAKELOCK = 0;
+ public static final int PLAY_SONG = 3;
+ public static final int PREPARE_NEXT = 4;
+ public static final int SET_POSITION = 5;
+ public static final int FOCUS_CHANGE = 6;
+ public static final int DUCK = 7;
+ public static final int UNDUCK = 8;
+
+ public static final int SHUFFLE_MODE_NONE = 0;
+ public static final int SHUFFLE_MODE_SHUFFLE = 1;
+
+ public static final int REPEAT_MODE_NONE = 0;
+ public static final int REPEAT_MODE_ALL = 1;
+ public static final int REPEAT_MODE_THIS = 2;
+
+ public static final int SAVE_QUEUE = 0;
+ public static final int LOAD_QUEUE = 9;
+
+ private final IBinder musicBinder = new MusicBinder();
+
+ public boolean pendingQuit = false;
+
+ private Playback playback;
+
+ private List playingQueue = new ArrayList<>();
+ private List originalPlayingQueue = new ArrayList<>();
+
+ private int position = -1;
+ private int nextPosition = -1;
+
+ private int repeatMode;
+
+ private boolean notHandledMetaChangedForCurrentTrack;
+ private boolean queuesRestored;
+ private boolean pausedByTransientLossOfFocus;
+
+ private PlayingNotification playingNotification;
private AudioManager audioManager;
- private MediaSource mediaSource;
- private SimpleExoPlayer player;
+ private MediaSessionCompat mediaSession;
+ private PowerManager.WakeLock wakeLock;
+
+ private PlaybackHandler playerHandler;
+ private Handler uiThreadHandler;
+ private ThrottledSeekHandler throttledSeekHandler;
+ private QueueHandler queueHandler;
+ private ProgressHandler progressHandler;
+
+ private HandlerThread playerHandlerThread;
+ private HandlerThread progressHandlerThread;
+ private HandlerThread queueHandlerThread;
+
+ private final BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, @NonNull Intent intent) {
+ if (intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
+ pause();
+ }
+ }
+ };
+
+ private final AudioManager.OnAudioFocusChangeListener audioFocusListener = new AudioManager.OnAudioFocusChangeListener() {
+ @Override
+ public void onAudioFocusChange(final int focusChange) {
+ playerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget();
+ }
+ };
+
+ private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_SEEK_TO;
@Override
public void onCreate() {
- audioManager = (AudioManager) getApplicationContext().getSystemService(AUDIO_SERVICE);
super.onCreate();
+
+ final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
+ wakeLock.setReferenceCounted(false);
+
+ playback = new MultiPlayer(this);
+ playback.setCallbacks(this);
+
+ playerHandlerThread = new HandlerThread(PlaybackHandler.class.getName());
+ playerHandlerThread.start();
+ playerHandler = new PlaybackHandler(this, playerHandlerThread.getLooper());
+
+ progressHandlerThread = new HandlerThread(ProgressHandler.class.getName());
+ progressHandlerThread.start();
+ progressHandler = new ProgressHandler(this, progressHandlerThread.getLooper());
+
+ queueHandlerThread = new HandlerThread(QueueHandler.class.getName(), Process.THREAD_PRIORITY_BACKGROUND);
+ queueHandlerThread.start();
+ queueHandler = new QueueHandler(this, queueHandlerThread.getLooper());
+
+ throttledSeekHandler = new ThrottledSeekHandler(playerHandler);
+ uiThreadHandler = new Handler();
+
+ registerReceiver(becomingNoisyReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+
+ initNotification();
+ initMediaSession();
+ restoreState();
+
+ mediaSession.setActive(true);
+ }
+
+ private AudioManager getAudioManager() {
+ if (audioManager == null) {
+ audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ return audioManager;
+ }
+
+ private void initMediaSession() {
+ ComponentName mediaButtonReceiverComponentName = new ComponentName(getApplicationContext(), MediaButtonIntentReceiver.class);
+
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mediaButtonReceiverComponentName);
+
+ PendingIntent mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0);
+
+ mediaSession = new MediaSessionCompat(this, getResources().getString(R.string.app_name), mediaButtonReceiverComponentName, mediaButtonReceiverPendingIntent);
+ mediaSession.setCallback(new MediaSessionCompat.Callback() {
+ @Override
+ public void onPlay() {
+ play();
+ }
+
+ @Override
+ public void onPause() {
+ pause();
+ }
+
+ @Override
+ public void onSkipToNext() {
+ playNextSong(true);
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ back(true);
+ }
+
+ @Override
+ public void onStop() {
+ quit();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ seek((int) pos);
+ }
+
+ @Override
+ public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
+ return MediaButtonIntentReceiver.handleIntent(MusicService.this, mediaButtonEvent);
+ }
+ });
+
+ mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent);
}
@Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- String input = intent.getStringExtra("playStop");
- if (input != null && input.equals("play")) {
- if (requestFocus()) {
- initPlayer();
- play();
+ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+ if (intent != null) {
+ if (intent.getAction() != null) {
+ String action = intent.getAction();
+ switch (action) {
+ case ACTION_TOGGLE:
+ if (isPlaying()) {
+ pause();
+ } else {
+ play();
+ }
+ break;
+ case ACTION_PAUSE:
+ pause();
+ break;
+ case ACTION_PLAY:
+ play();
+ break;
+ case ACTION_PLAY_PLAYLIST:
+ Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST);
+ if (playlist != null) {
+ List playlistSongs = new ArrayList<>();
+ if (!playlistSongs.isEmpty()) {
+ openQueue(playlistSongs, 0, true);
+ } else {
+ Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show();
+ }
+ break;
+ case ACTION_REWIND:
+ back(true);
+ break;
+ case ACTION_SKIP:
+ playNextSong(true);
+ break;
+ case ACTION_STOP:
+ case ACTION_QUIT:
+ pendingQuit = false;
+ quit();
+ break;
+ case ACTION_PENDING_QUIT:
+ pendingQuit = true;
+ break;
+ }
}
- } else {
- stop();
}
- startForeground(1, getNotification());
+
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
- stop();
- super.onDestroy();
+ unregisterReceiver(becomingNoisyReceiver);
+
+ progressHandler.sendEmptyMessage(TRACK_ENDED);
+ mediaSession.setActive(false);
+ quit();
+ releaseResources();
+ wakeLock.release();
}
- @Nullable
@Override
public IBinder onBind(Intent intent) {
- return null;
+ return musicBinder;
}
- private Notification getNotification() {
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
- .setContentText("Running ...")
- .setContentTitle("Play")
- .setOngoing(true)
- .setSmallIcon(R.drawable.exo_controls_shuffle_on)
- .setChannelId("PlayApp");
- return builder.build();
+ private static final class QueueHandler extends Handler {
+ @NonNull
+ private final WeakReference mService;
+
+ public QueueHandler(final MusicService service, @NonNull final Looper looper) {
+ super(looper);
+ mService = new WeakReference<>(service);
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ final MusicService service = mService.get();
+ switch (msg.what) {
+ case LOAD_QUEUE:
+ service.restoreQueuesAndPositionIfNecessary();
+ break;
+ case SAVE_QUEUE:
+ service.saveQueue();
+ break;
+ }
+ }
}
- @Override
- public void onAudioFocusChange(int focusChange) {
- if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS) {
- stop();
- System.exit(0);
+ private void saveQueue() {
+ App.getDatabase().songDao().deleteSongs();
+ App.getDatabase().songDao().insertSongs(playingQueue);
+
+ App.getDatabase().queueSongDao().deleteQueueSongs();
+ App.getDatabase().queueSongDao().setQueue(playingQueue, 0);
+ App.getDatabase().queueSongDao().setQueue(originalPlayingQueue, 1);
+ }
+
+ private void savePosition() {
+ PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(PreferenceUtil.POSITION, getPosition()).apply();
+ }
+
+ private void saveProgress() {
+ PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(PreferenceUtil.PROGRESS, getSongProgressMillis()).apply();
+ }
+
+ public void saveState() {
+ queueHandler.removeMessages(SAVE_QUEUE);
+ queueHandler.sendEmptyMessage(SAVE_QUEUE);
+
+ savePosition();
+ saveProgress();
+ }
+
+ private void restoreState() {
+ repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(PreferenceUtil.REPEAT, 0);
+
+ notifyChange(REPEAT_MODE_CHANGED);
+
+ queueHandler.removeMessages(LOAD_QUEUE);
+ queueHandler.sendEmptyMessage(LOAD_QUEUE);
+ }
+
+ private synchronized void restoreQueuesAndPositionIfNecessary() {
+ if (!queuesRestored && playingQueue.isEmpty()) {
+ List restoredQueue = App.getDatabase().queueSongDao().getQueue(0);
+ List restoredOriginalQueue = App.getDatabase().queueSongDao().getQueue(1);
+
+ int restoredPosition = PreferenceManager.getDefaultSharedPreferences(this).getInt(PreferenceUtil.POSITION, -1);
+ int restoredPositionInTrack = PreferenceManager.getDefaultSharedPreferences(this).getInt(PreferenceUtil.PROGRESS, -1);
+
+ if (restoredQueue.size() > 0 && restoredQueue.size() == restoredOriginalQueue.size() && restoredPosition != -1) {
+ this.originalPlayingQueue = restoredOriginalQueue;
+ this.playingQueue = restoredQueue;
+
+ position = restoredPosition;
+ openCurrent();
+
+ if (restoredPositionInTrack > 0) seek(restoredPositionInTrack);
+
+ notHandledMetaChangedForCurrentTrack = true;
+ sendChangeInternal(META_CHANGED);
+ sendChangeInternal(QUEUE_CHANGED);
+ }
+ }
+
+ queuesRestored = true;
+ }
+
+ private void quit() {
+ pause();
+ playingNotification.stop();
+
+ getAudioManager().abandonAudioFocus(audioFocusListener);
+ stopSelf();
+ }
+
+ private void releaseResources() {
+ playerHandler.removeCallbacksAndMessages(null);
+ playerHandlerThread.quitSafely();
+
+ progressHandler.removeCallbacksAndMessages(null);
+ progressHandlerThread.quitSafely();
+
+ queueHandler.removeCallbacksAndMessages(null);
+ queueHandlerThread.quitSafely();
+
+ playback.stop();
+ mediaSession.release();
+ }
+
+ public boolean isPlaying() {
+ return playback != null && playback.isPlaying();
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public void playNextSong(boolean force) {
+ playSongAt(getNextPosition(force));
+ }
+
+ private void openTrackAndPrepareNextAt(int position) {
+ synchronized (this) {
+ this.position = position;
+
+ openCurrent();
+ playback.start();
+
+ notifyChange(META_CHANGED);
+ notHandledMetaChangedForCurrentTrack = false;
+ }
+ }
+
+ private void openCurrent() {
+ synchronized (this) {
+ // current song will be null when queue is cleared
+ if (getCurrentSong() == null) return;
+
+ playback.setDataSource(getCurrentSong());
+ }
+ }
+
+ private void prepareNext() {
+ playerHandler.removeMessages(PREPARE_NEXT);
+ playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget();
+ }
+
+ private void prepareNextImpl() {
+ synchronized (this) {
+ nextPosition = getNextPosition(false);
+ playback.queueDataSource(getSongAt(nextPosition));
}
}
private boolean requestFocus() {
- return (audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ return getAudioManager().requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
- private void play() {
- player.setForegroundMode(true);
- player.prepare(mediaSource);
- player.setPlayWhenReady(true);
+ public void initNotification() {
+ playingNotification = new PlayingNotification();
+
+ playingNotification.init(this);
}
- private void stop() {
- player.setPlayWhenReady(false);
- player.stop();
+ public void updateNotification() {
+ if (playingNotification != null && getCurrentSong().getId() != null) {
+ playingNotification.update();
+ }
}
- private void initPlayer() {
- player = ExoPlayerFactory.newSimpleInstance(getApplicationContext());
- DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(getApplicationContext(), "exoPlayerSample"));
- mediaSource = new ProgressiveMediaSource.Factory(defaultDataSourceFactory).createMediaSource(Uri.parse("http://192.168.1.81:8096/Audio/5656e9fd11e38ba95ba4871bc061991a/universal?UserId=34addd030b4545e5ac4300dc322c9f73&DeviceId=e40853e4e7ab76f1&MaxStreamingBitrate=10000000&Container=flac|flac,mp3|mp3,opus|opus,m4a|aac,ogg|vorbis,ogg|opus,mka|opus&TranscodingContainer=ts&TranscodingProtocol=hls&AudioCodec=aac&api_key=7e6626ca220d4b01961022e148868d41"));
+ private void updateMediaSessionState() {
+ mediaSession.setPlaybackState(
+ new PlaybackStateCompat.Builder()
+ .setActions(MEDIA_SESSION_ACTIONS)
+ .setState(isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, getSongProgressMillis(), 1)
+ .build());
+ }
+
+ @SuppressLint("CheckResult")
+ private void updateMediaSessionMetadata() {
+ final Song song = getCurrentSong();
+
+ if (song.getId() == null) {
+ mediaSession.setMetadata(null);
+ return;
+ }
+
+ final MediaMetadataCompat.Builder metaData = new MediaMetadataCompat.Builder()
+ .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getArtistName())
+ .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.getArtistName())
+ .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbumName())
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getTitle())
+ .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.getDuration())
+ .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, getPosition() + 1)
+ .putLong(MediaMetadataCompat.METADATA_KEY_YEAR, song.getYear())
+ .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null);
+
+ metaData.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getPlayingQueue().size());
+ }
+
+ public void runOnUiThread(Runnable runnable) {
+ uiThreadHandler.post(runnable);
+ }
+
+ public Song getCurrentSong() {
+ return getSongAt(getPosition());
+ }
+
+ public Song getSongAt(int position) {
+ return getPlayingQueue().get(position);
+ }
+
+ public int getNextPosition(boolean force) {
+ int position = getPosition() + 1;
+ switch (getRepeatMode()) {
+ case REPEAT_MODE_ALL:
+ if (isLastTrack()) {
+ position = 0;
+ }
+ break;
+ case REPEAT_MODE_THIS:
+ if (force) {
+ if (isLastTrack()) {
+ position = 0;
+ }
+ } else {
+ position -= 1;
+ }
+ break;
+ default:
+ case REPEAT_MODE_NONE:
+ if (isLastTrack()) {
+ position -= 1;
+ }
+ break;
+ }
+
+ return position;
+ }
+
+ private boolean isLastTrack() {
+ return getPosition() == getPlayingQueue().size() - 1;
+ }
+
+ public List getPlayingQueue() {
+ return playingQueue;
+ }
+
+ public int getRepeatMode() {
+ return repeatMode;
+ }
+
+ public void setRepeatMode(final int repeatMode) {
+ switch (repeatMode) {
+ case REPEAT_MODE_NONE:
+ case REPEAT_MODE_ALL:
+ case REPEAT_MODE_THIS:
+ this.repeatMode = repeatMode;
+ PreferenceManager.getDefaultSharedPreferences(this).edit()
+ .putInt(PreferenceUtil.REPEAT, repeatMode)
+ .apply();
+ prepareNext();
+ notifyChange(REPEAT_MODE_CHANGED);
+ break;
+ }
+ }
+
+ public void openQueue(@Nullable final List playingQueue, final int startPosition, final boolean startPlaying) {
+ if (playingQueue != null && !playingQueue.isEmpty() && startPosition >= 0 && startPosition < playingQueue.size()) {
+ // it is important to copy the playing queue here first as we might add or remove songs later
+ originalPlayingQueue = new ArrayList<>(playingQueue);
+ this.playingQueue = new ArrayList<>(originalPlayingQueue);
+
+ if (startPlaying) {
+ playSongAt(startPosition);
+ } else {
+ setPosition(startPosition);
+ }
+
+ notifyChange(QUEUE_CHANGED);
+ }
+ }
+
+ public void addSong(int position, Song song) {
+ playingQueue.add(position, song);
+ originalPlayingQueue.add(position, song);
+ notifyChange(QUEUE_CHANGED);
+ }
+
+ public void addSong(Song song) {
+ playingQueue.add(song);
+ originalPlayingQueue.add(song);
+ notifyChange(QUEUE_CHANGED);
+ }
+
+ public void addSongs(int position, List songs) {
+ playingQueue.addAll(position, songs);
+ originalPlayingQueue.addAll(position, songs);
+ notifyChange(QUEUE_CHANGED);
+ }
+
+ public void addSongs(List songs) {
+ playingQueue.addAll(songs);
+ originalPlayingQueue.addAll(songs);
+ notifyChange(QUEUE_CHANGED);
+ }
+
+ public void removeSong(int position) {
+ originalPlayingQueue.remove(playingQueue.remove(position));
+ reposition(position);
+ notifyChange(QUEUE_CHANGED);
+ }
+
+ private void reposition(int deletedPosition) {
+ int currentPosition = getPosition();
+ if (deletedPosition < currentPosition) {
+ position = currentPosition - 1;
+ } else if (deletedPosition == currentPosition) {
+ if (playingQueue.size() > deletedPosition) {
+ setPosition(position);
+ } else {
+ setPosition(position - 1);
+ }
+ }
+ }
+
+ public void moveSong(int from, int to) {
+ if (from == to) return;
+ final int currentPosition = getPosition();
+ Song songToMove = playingQueue.remove(from);
+ playingQueue.add(to, songToMove);
+
+ if (from > currentPosition && to <= currentPosition) {
+ position = currentPosition + 1;
+ } else if (from < currentPosition && to >= currentPosition) {
+ position = currentPosition - 1;
+ } else if (from == currentPosition) {
+ position = to;
+ }
+
+ notifyChange(QUEUE_CHANGED);
+ }
+
+ public void clearQueue() {
+ playingQueue.clear();
+ originalPlayingQueue.clear();
+
+ setPosition(-1);
+ notifyChange(QUEUE_CHANGED);
+ }
+
+ public void playSongAt(final int position) {
+ // handle this on the handlers thread to avoid blocking the ui thread
+ playerHandler.removeMessages(PLAY_SONG);
+ playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget();
+ }
+
+ public void setPosition(final int position) {
+ // handle this on the handlers thread to avoid blocking the ui thread
+ playerHandler.removeMessages(SET_POSITION);
+ playerHandler.obtainMessage(SET_POSITION, position, 0).sendToTarget();
+ }
+
+ private void playSongAtImpl(int position) {
+ openTrackAndPrepareNextAt(position);
+ }
+
+ public void pause() {
+ pausedByTransientLossOfFocus = false;
+ if (playback.isPlaying()) {
+ playback.pause();
+ notifyChange(STATE_CHANGED);
+ }
+ }
+
+ public void play() {
+ synchronized (this) {
+ if (requestFocus()) {
+ if (!playback.isPlaying()) {
+ if (!playback.isReady()) {
+ playSongAt(getPosition());
+ } else {
+ playback.start();
+ if (notHandledMetaChangedForCurrentTrack) {
+ handleChangeInternal(META_CHANGED);
+ notHandledMetaChangedForCurrentTrack = false;
+ }
+
+ notifyChange(STATE_CHANGED);
+
+ // fixes a bug where the volume would stay ducked
+ // happens when audio focus GAIN event not sent
+ playerHandler.removeMessages(DUCK);
+ playerHandler.sendEmptyMessage(UNDUCK);
+ }
+ }
+ } else {
+ Toast.makeText(this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ public void playPreviousSong(boolean force) {
+ playSongAt(getPreviousPosition(force));
+ }
+
+ public void back(boolean force) {
+ if (getSongProgressMillis() > 5000) {
+ seek(0);
+ } else {
+ playPreviousSong(force);
+ }
+ }
+
+ public int getPreviousPosition(boolean force) {
+ int newPosition = getPosition() - 1;
+ switch (repeatMode) {
+ case REPEAT_MODE_ALL:
+ if (newPosition < 0) {
+ newPosition = getPlayingQueue().size() - 1;
+ }
+ break;
+ case REPEAT_MODE_THIS:
+ if (force) {
+ if (newPosition < 0) {
+ newPosition = getPlayingQueue().size() - 1;
+ }
+ } else {
+ newPosition = getPosition();
+ }
+ break;
+ default:
+ case REPEAT_MODE_NONE:
+ if (newPosition < 0) {
+ newPosition = 0;
+ }
+ break;
+ }
+
+ return newPosition;
+ }
+
+ public int getSongProgressMillis() {
+ return playback.getProgress();
+ }
+
+ public int getSongDurationMillis() {
+ return playback.getDuration();
+ }
+
+ public long getQueueDurationMillis(int position) {
+ long duration = 0;
+ for (int i = position + 1; i < playingQueue.size(); i++) {
+ duration += playingQueue.get(i).getDuration();
+ }
+
+ return duration;
+ }
+
+ public int seek(int millis) {
+ synchronized (this) {
+ playback.setProgress(millis);
+ throttledSeekHandler.notifySeek();
+ return millis;
+ }
+ }
+
+ public void cycleRepeatMode() {
+ switch (getRepeatMode()) {
+ case REPEAT_MODE_NONE:
+ setRepeatMode(REPEAT_MODE_ALL);
+ break;
+ case REPEAT_MODE_ALL:
+ setRepeatMode(REPEAT_MODE_THIS);
+ break;
+ default:
+ setRepeatMode(REPEAT_MODE_NONE);
+ break;
+ }
+ }
+
+ private void notifyChange(@NonNull final String what) {
+ handleChangeInternal(what);
+ sendChangeInternal(what);
+ }
+
+ private void sendChangeInternal(final String what) {
+ sendBroadcast(new Intent(what));
+ }
+
+ private void handleChangeInternal(@NonNull final String what) {
+ switch (what) {
+ case STATE_CHANGED:
+ updateNotification();
+ updateMediaSessionState();
+ if (!isPlaying()) saveProgress();
+ break;
+ case META_CHANGED:
+ updateNotification();
+ updateMediaSessionMetadata();
+ updateMediaSessionState();
+ savePosition();
+ saveProgress();
+ break;
+ case QUEUE_CHANGED:
+ // because playing queue size might have changed
+ updateMediaSessionMetadata();
+ saveState();
+ if (playingQueue.size() > 0) {
+ prepareNext();
+ } else {
+ playingNotification.stop();
+ }
+ break;
+ }
+ }
+
+ public MediaSessionCompat getMediaSession() {
+ return mediaSession;
+ }
+
+ public void releaseWakeLock() {
+ if (wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ }
+
+ public void acquireWakeLock(long milli) {
+ wakeLock.acquire(milli);
+ }
+
+ @Override
+ public void onTrackStarted() {
+ progressHandler.sendEmptyMessage(TRACK_STARTED);
+
+ notifyChange(STATE_CHANGED);
+ prepareNext();
+ }
+
+ @Override
+ public void onTrackWentToNext() {
+ playerHandler.sendEmptyMessage(TRACK_CHANGED);
+ progressHandler.sendEmptyMessage(TRACK_CHANGED);
+
+ acquireWakeLock(30000);
+ }
+
+ @Override
+ public void onTrackEnded() {
+ playerHandler.sendEmptyMessage(TRACK_ENDED);
+ progressHandler.sendEmptyMessage(TRACK_ENDED);
+ }
+
+ private static final class PlaybackHandler extends Handler {
+ private final WeakReference mService;
+ private int currentDuckVolume = 100;
+
+ public PlaybackHandler(final MusicService service, @NonNull final Looper looper) {
+ super(looper);
+ mService = new WeakReference<>(service);
+ }
+
+ @Override
+ public void handleMessage(@NonNull final Message msg) {
+ final MusicService service = mService.get();
+ if (service == null) {
+ return;
+ }
+
+ switch (msg.what) {
+ case DUCK:
+ if (PreferenceUtil.getInstance(service).getAudioDucking()) {
+ currentDuckVolume -= 5;
+ if (currentDuckVolume > 20) {
+ sendEmptyMessageDelayed(DUCK, 10);
+ } else {
+ currentDuckVolume = 20;
+ }
+ } else {
+ currentDuckVolume = 100;
+ }
+
+ service.playback.setVolume(currentDuckVolume);
+ break;
+
+ case UNDUCK:
+ if (PreferenceUtil.getInstance(service).getAudioDucking()) {
+ currentDuckVolume += 3;
+ if (currentDuckVolume < 100) {
+ sendEmptyMessageDelayed(UNDUCK, 10);
+ } else {
+ currentDuckVolume = 100;
+ }
+ } else {
+ currentDuckVolume = 100;
+ }
+
+ service.playback.setVolume(currentDuckVolume);
+ break;
+
+ case TRACK_CHANGED:
+ if (service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
+ service.pause();
+ service.seek(0);
+ service.notifyChange(STATE_CHANGED);
+ } else {
+ service.position = service.nextPosition;
+ service.prepareNextImpl();
+ service.notifyChange(META_CHANGED);
+ }
+ break;
+
+ case TRACK_ENDED:
+ // if there is a timer finished, don't continue
+ if (service.pendingQuit || service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
+ service.notifyChange(STATE_CHANGED);
+ service.seek(0);
+ if (service.pendingQuit) {
+ service.pendingQuit = false;
+ service.quit();
+ break;
+ }
+ } else {
+ service.playNextSong(false);
+ }
+
+ sendEmptyMessage(RELEASE_WAKELOCK);
+ break;
+
+ case RELEASE_WAKELOCK:
+ service.releaseWakeLock();
+ break;
+
+ case PLAY_SONG:
+ service.playSongAtImpl(msg.arg1);
+ service.notifyChange(STATE_CHANGED);
+ break;
+
+ case SET_POSITION:
+ service.openTrackAndPrepareNextAt(msg.arg1);
+ service.notifyChange(STATE_CHANGED);
+ break;
+
+ case PREPARE_NEXT:
+ service.prepareNextImpl();
+ break;
+
+ case FOCUS_CHANGE:
+ switch (msg.arg1) {
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (!service.isPlaying() && service.pausedByTransientLossOfFocus) {
+ service.play();
+ service.pausedByTransientLossOfFocus = false;
+ }
+ removeMessages(DUCK);
+ sendEmptyMessage(UNDUCK);
+ break;
+
+ case AudioManager.AUDIOFOCUS_LOSS:
+ // Lost focus for an unbounded amount of time: stop playback and release media playback
+ service.pause();
+ break;
+
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ // Lost focus for a short time, but we have to stop
+ // playback. We don't release the media playback because playback
+ // is likely to resume
+ boolean wasPlaying = service.isPlaying();
+ service.pause();
+ service.pausedByTransientLossOfFocus = wasPlaying;
+ break;
+
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ // Lost focus for a short time, but it's ok to keep playing
+ // at an attenuated level
+ removeMessages(UNDUCK);
+ sendEmptyMessage(DUCK);
+ break;
+ }
+ break;
+ }
+ }
+ }
+
+ public class MusicBinder extends Binder {
+ @NonNull
+ public MusicService getService() {
+ return MusicService.this;
+ }
+ }
+
+ private class ThrottledSeekHandler implements Runnable {
+ // milliseconds to throttle before calling run to aggregate events
+ private static final long THROTTLE = 500;
+ private final Handler mHandler;
+
+ public ThrottledSeekHandler(Handler handler) {
+ mHandler = handler;
+ }
+
+ public void notifySeek() {
+ mHandler.removeCallbacks(this);
+ mHandler.postDelayed(this, THROTTLE);
+ }
+
+ @Override
+ public void run() {
+ notifyChange(STATE_CHANGED);
+ }
+ }
+
+ private static final class ProgressHandler extends Handler {
+ private final WeakReference mService;
+
+ private ScheduledExecutorService executorService;
+ private Future> task;
+
+ public ProgressHandler(MusicService service, Looper looper) {
+ super(looper);
+
+ mService = new WeakReference<>(service);
+ }
+
+ @Override
+ public void handleMessage(@NonNull final Message msg) {
+ switch (msg.what) {
+ case TRACK_STARTED:
+ onStart();
+ case TRACK_CHANGED:
+ onNext();
+ break;
+ case TRACK_ENDED:
+ onStop();
+ }
+ }
+
+ public void onStart() {
+ if (executorService != null) executorService.shutdownNow();
+
+ executorService = Executors.newScheduledThreadPool(1);
+ task = executorService.scheduleAtFixedRate(this::onProgress, 10, 10, TimeUnit.SECONDS);
+ }
+
+ public void onNext() {
+ PlaybackStartInfo startInfo = new PlaybackStartInfo();
+
+ startInfo.setItemId(mService.get().getCurrentSong().getId());
+ startInfo.setVolumeLevel(mService.get().playback.getVolume());
+ startInfo.setCanSeek(true);
+ startInfo.setIsPaused(false);
+
+ App.getInstance().getApiClientInstance(App.getInstance()).ensureWebSocket();
+ App.getInstance().getApiClientInstance(App.getInstance()).ReportPlaybackStartAsync(startInfo, new EmptyResponse());
+ }
+
+ public void onProgress() {
+ PlaybackProgressInfo progressInfo = new PlaybackProgressInfo();
+
+ // TODO these cause a wrong thread error
+ long progress = mService.get().getSongProgressMillis();
+ double duration = mService.get().getSongDurationMillis();
+ if (progress / duration > 0.9) {
+ Song current = mService.get().getCurrentSong();
+ String user = App.getInstance().getApiClientInstance(App.getInstance()).getCurrentUserId();
+ Date time = new Date(System.currentTimeMillis());
+
+ App.getInstance().getApiClientInstance(App.getInstance()).MarkPlayedAsync(current.getId(), user, time, new Response<>());
+ }
+
+ progressInfo.setItemId(mService.get().getCurrentSong().getId());
+ progressInfo.setPositionTicks(progress * 10000);
+ progressInfo.setVolumeLevel(mService.get().playback.getVolume());
+ progressInfo.setIsPaused(!mService.get().playback.isPlaying());
+ progressInfo.setCanSeek(true);
+
+ App.getInstance().getApiClientInstance(App.getInstance()).ReportPlaybackProgressAsync(progressInfo, new EmptyResponse());
+ }
+
+ public void onStop() {
+ PlaybackStopInfo info = new PlaybackStopInfo();
+ long progress = mService.get().getSongProgressMillis();
+
+ info.setItemId(mService.get().getCurrentSong().getId());
+ info.setPositionTicks(progress * 10000);
+
+ task.cancel(true);
+ executorService.shutdownNow();
+ }
}
}
diff --git a/app/src/main/java/com/cappielloantonio/play/service/notification/PlayingNotification.java b/app/src/main/java/com/cappielloantonio/play/service/notification/PlayingNotification.java
new file mode 100644
index 00000000..d98fa520
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/service/notification/PlayingNotification.java
@@ -0,0 +1,181 @@
+package com.cappielloantonio.play.service.notification;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+import androidx.palette.graphics.Palette;
+
+import com.bumptech.glide.request.target.CustomTarget;
+import com.bumptech.glide.request.transition.Transition;
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.glide.CustomGlideRequest;
+import com.cappielloantonio.play.glide.palette.BitmapPaletteWrapper;
+import com.cappielloantonio.play.model.Song;
+import com.cappielloantonio.play.service.MusicService;
+import com.cappielloantonio.play.ui.activities.MainActivity;
+
+import static android.content.Context.NOTIFICATION_SERVICE;
+import static com.cappielloantonio.play.service.MusicService.ACTION_REWIND;
+import static com.cappielloantonio.play.service.MusicService.ACTION_SKIP;
+import static com.cappielloantonio.play.service.MusicService.ACTION_TOGGLE;
+
+public class PlayingNotification {
+
+ private static final int NOTIFICATION_ID = 1;
+ static final String NOTIFICATION_CHANNEL_ID = "playing_notification";
+
+ private static final int NOTIFY_MODE_FOREGROUND = 1;
+ private static final int NOTIFY_MODE_BACKGROUND = 0;
+
+ private int notifyMode = NOTIFY_MODE_BACKGROUND;
+
+ private NotificationManager notificationManager;
+ protected MusicService service;
+ boolean stopped;
+
+ public synchronized void init(MusicService service) {
+ this.service = service;
+ notificationManager = (NotificationManager) service.getSystemService(NOTIFICATION_SERVICE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel();
+ }
+ }
+
+ public synchronized void update() {
+ stopped = false;
+
+ final Song song = service.getCurrentSong();
+
+ final boolean isPlaying = service.isPlaying();
+
+ final int playButtonResId = isPlaying ? R.drawable.ic_pause_white_24dp : R.drawable.ic_play_arrow_white_24dp;
+
+ Intent action = new Intent(service, MainActivity.class);
+ action.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ final PendingIntent clickIntent = PendingIntent.getActivity(service, 0, action, 0);
+
+ final ComponentName serviceName = new ComponentName(service, MusicService.class);
+ Intent intent = new Intent(MusicService.ACTION_QUIT);
+ intent.setComponent(serviceName);
+ final PendingIntent deleteIntent = PendingIntent.getService(service, 0, intent, 0);
+
+ final int bigNotificationImageSize = service.getResources().getDimensionPixelSize(R.dimen.notification_big_image_size);
+ service.runOnUiThread(() -> CustomGlideRequest.Builder
+ .from(service, song.getPrimary(), song.getBlurHash(), CustomGlideRequest.PRIMARY, CustomGlideRequest.TOP_QUALITY)
+ .build()
+ .into(new CustomTarget(bigNotificationImageSize, bigNotificationImageSize) {
+ @Override
+ public void onResourceReady(@NonNull BitmapPaletteWrapper resource, Transition super BitmapPaletteWrapper> glideAnimation) {
+ Palette palette = resource.getPalette();
+ update(resource.getBitmap(), palette.getVibrantColor(palette.getMutedColor(Color.TRANSPARENT)));
+ }
+
+ @Override
+ public void onLoadFailed(Drawable drawable) {
+ update(null, Color.TRANSPARENT);
+ }
+
+ @Override
+ public void onLoadCleared(Drawable drawable) {
+ update(null, Color.TRANSPARENT);
+ }
+
+ void update(Bitmap bitmap, int color) {
+ if (bitmap == null)
+ bitmap = BitmapFactory.decodeResource(service.getResources(), R.drawable.default_album_art);
+ NotificationCompat.Action playPauseAction = new NotificationCompat.Action(playButtonResId,
+ service.getString(R.string.action_play_pause),
+ retrievePlaybackAction(ACTION_TOGGLE));
+ NotificationCompat.Action previousAction = new NotificationCompat.Action(R.drawable.ic_skip_previous_white_24dp,
+ service.getString(R.string.action_previous),
+ retrievePlaybackAction(ACTION_REWIND));
+ NotificationCompat.Action nextAction = new NotificationCompat.Action(R.drawable.ic_skip_next_white_24dp,
+ service.getString(R.string.action_next),
+ retrievePlaybackAction(ACTION_SKIP));
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setSubText(song.getAlbumName())
+ .setLargeIcon(bitmap)
+ .setContentIntent(clickIntent)
+ .setDeleteIntent(deleteIntent)
+ .setContentTitle(song.getTitle())
+ .setContentText(song.getArtistName())
+ .setOngoing(isPlaying)
+ .setShowWhen(false)
+ .addAction(previousAction)
+ .addAction(playPauseAction)
+ .addAction(nextAction);
+
+ builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle().setMediaSession(service.getMediaSession().getSessionToken())
+ .setShowActionsInCompactView(0, 1, 2))
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setColor(color);
+
+ // notification has been stopped before loading was finished
+ if (stopped) return;
+
+ updateNotifyModeAndPostNotification(builder.build());
+ }
+ }));
+ }
+
+ public synchronized void stop() {
+ stopped = true;
+ service.stopForeground(true);
+ notificationManager.cancel(NOTIFICATION_ID);
+ }
+
+ private PendingIntent retrievePlaybackAction(final String action) {
+ final ComponentName serviceName = new ComponentName(service, MusicService.class);
+ Intent intent = new Intent(action);
+ intent.setComponent(serviceName);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ return PendingIntent.getService(service, 0, intent, 0);
+ }
+
+ void updateNotifyModeAndPostNotification(Notification notification) {
+ int newNotifyMode;
+ if (service.isPlaying()) {
+ newNotifyMode = NOTIFY_MODE_FOREGROUND;
+ } else {
+ newNotifyMode = NOTIFY_MODE_BACKGROUND;
+ }
+
+ if (notifyMode != newNotifyMode && newNotifyMode == NOTIFY_MODE_BACKGROUND) {
+ service.stopForeground(false);
+ }
+
+ if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
+ service.startForeground(NOTIFICATION_ID, notification);
+ } else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ }
+
+ notifyMode = newNotifyMode;
+ }
+
+ @RequiresApi(26)
+ private void createNotificationChannel() {
+ NotificationChannel notificationChannel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID);
+ if (notificationChannel == null) {
+ notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, service.getString(R.string.playing_notification_name), NotificationManager.IMPORTANCE_LOW);
+ notificationChannel.setDescription(service.getString(R.string.playing_notification_description));
+ notificationChannel.enableLights(false);
+ notificationChannel.enableVibration(false);
+
+ notificationManager.createNotificationChannel(notificationChannel);
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/service/playback/Playback.java b/app/src/main/java/com/cappielloantonio/play/service/playback/Playback.java
new file mode 100644
index 00000000..7cb48bcb
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/service/playback/Playback.java
@@ -0,0 +1,39 @@
+package com.cappielloantonio.play.service.playback;
+
+import com.cappielloantonio.play.model.Song;
+
+public interface Playback {
+ void setDataSource(Song song);
+
+ void queueDataSource(Song song);
+
+ void setCallbacks(PlaybackCallbacks callbacks);
+
+ boolean isReady();
+
+ boolean isPlaying();
+
+ void start();
+
+ void pause();
+
+ void stop();
+
+ int getProgress();
+
+ int getDuration();
+
+ void setProgress(int progress);
+
+ void setVolume(int volume);
+
+ int getVolume();
+
+ interface PlaybackCallbacks {
+ void onTrackStarted();
+
+ void onTrackWentToNext();
+
+ void onTrackEnded();
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java b/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java
index 8ddc16a9..ccab8556 100644
--- a/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java
+++ b/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java
@@ -135,7 +135,7 @@ public class MainActivity extends BaseActivity {
* lo chiudo
*/
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
- if(bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && (
+ if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && (
destination.getId() == R.id.homeFragment ||
destination.getId() == R.id.libraryFragment ||
destination.getId() == R.id.searchFragment ||
@@ -164,32 +164,32 @@ public class MainActivity extends BaseActivity {
}
private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
- new BottomSheetBehavior.BottomSheetCallback() {
- @Override
- public void onStateChanged(@NonNull View view, int state) {
- switch (state) {
- case BottomSheetBehavior.STATE_SETTLING:
- PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
- if(playerBottomSheetFragment == null) break;
+ new BottomSheetBehavior.BottomSheetCallback() {
+ @Override
+ public void onStateChanged(@NonNull View view, int state) {
+ switch (state) {
+ case BottomSheetBehavior.STATE_SETTLING:
+ PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
+ if (playerBottomSheetFragment == null) break;
- playerBottomSheetFragment.scrollOnTop();
- break;
- case BottomSheetBehavior.STATE_HIDDEN:
- mainViewModel.deleteQueue();
- break;
+ playerBottomSheetFragment.scrollOnTop();
+ break;
+ case BottomSheetBehavior.STATE_HIDDEN:
+ mainViewModel.deleteQueue();
+ break;
+ }
}
- }
- @Override
- public void onSlide(@NonNull View view, float slideOffset) {
- PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
- if(playerBottomSheetFragment == null) return;
+ @Override
+ public void onSlide(@NonNull View view, float slideOffset) {
+ PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
+ if (playerBottomSheetFragment == null) return;
- float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f;
- playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset);
- playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE);
- }
- };
+ float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f;
+ playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset);
+ playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE);
+ }
+ };
/*
* Scroll on top del bottom sheet quando chiudo
@@ -197,7 +197,7 @@ public class MainActivity extends BaseActivity {
*/
public void setBottomSheetMusicInfo(Song song) {
PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
- if(playerBottomSheetFragment == null) return;
+ if (playerBottomSheetFragment == null) return;
playerBottomSheetFragment.scrollPager(song, 0, false);
}
@@ -246,7 +246,7 @@ public class MainActivity extends BaseActivity {
@Override
public void onBackPressed() {
- if(bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
+ if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
else
super.onBackPressed();
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerBottomSheetFragment.java
index ca6f9fcf..2e958ff7 100644
--- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerBottomSheetFragment.java
+++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlayerBottomSheetFragment.java
@@ -5,7 +5,6 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -20,7 +19,6 @@ import com.cappielloantonio.play.adapter.PlayerSongQueueAdapter;
import com.cappielloantonio.play.databinding.FragmentPlayerBottomSheetBinding;
import com.cappielloantonio.play.model.Song;
import com.cappielloantonio.play.ui.activities.MainActivity;
-import com.cappielloantonio.play.util.MusicUtil;
import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel;
public class PlayerBottomSheetFragment extends Fragment {
@@ -98,7 +96,7 @@ public class PlayerBottomSheetFragment extends Fragment {
}
private void playSong(Song song) {
- Toast.makeText(activity, MusicUtil.getSongFileUri(song), Toast.LENGTH_SHORT).show();
+ // Toast.makeText(activity, MusicUtil.getSongFileUri(song), Toast.LENGTH_SHORT).show();
}
public View getPlayerHeader() {
diff --git a/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java b/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java
index c101408a..a00bfbf2 100644
--- a/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java
+++ b/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java
@@ -19,18 +19,24 @@ public class PreferenceUtil {
public static final String TOKEN = "token";
public static final String MUSIC_LIBRARY_ID = "music_library_id";
+ public static final String SHUFFLE = "shuffle";
+ public static final String REPEAT = "repeat";
+ public static final String POSITION = "position";
+ public static final String PROGRESS = "progress";
+
public static final String SYNC = "sync";
public static final String SONG_GENRE_SYNC = "song_genre_sync";
public static final String HOST_URL = "host";
public static final String IMAGE_CACHE_SIZE = "image_cache_size";
+ public static final String MEDIA_CACHE_SIZE = "media_cache_size";
public static final String TRANSCODE_CODEC = "transcode_codec";
public static final String DIRECT_PLAY_CODECS = "direct_play_codecs";
public static final String MAXIMUM_BITRATE = "maximum_bitrate";
+ public static final String AUDIO_DUCKING = "audio_ducking";
private static PreferenceUtil sInstance;
-
private final SharedPreferences mPreferences;
private PreferenceUtil(final Context context) {
@@ -156,4 +162,21 @@ public class PreferenceUtil {
editor.putStringSet(DIRECT_PLAY_CODECS, codecNames);
editor.apply();
}
+
+ public final int getMediaCacheSize() {
+ return Integer.parseInt(mPreferences.getString(MEDIA_CACHE_SIZE, "400000000"));
+ }
+
+ public final boolean getAudioDucking() {
+ return mPreferences.getBoolean(AUDIO_DUCKING, true);
+ }
+
+
+ public void registerOnSharedPreferenceChangedListener(SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
+ mPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
+ }
+
+ public void unregisterOnSharedPreferenceChangedListener(SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
+ mPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_notification.png b/app/src/main/res/drawable/ic_notification.png
new file mode 100644
index 00000000..a401d50f
Binary files /dev/null and b/app/src/main/res/drawable/ic_notification.png differ
diff --git a/app/src/main/res/drawable/ic_pause_white_24dp.xml b/app/src/main/res/drawable/ic_pause_white_24dp.xml
new file mode 100644
index 00000000..660fbd6b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pause_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml b/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml
new file mode 100644
index 00000000..69301a3c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_skip_next_white_24dp.xml b/app/src/main/res/drawable/ic_skip_next_white_24dp.xml
new file mode 100644
index 00000000..473f8513
--- /dev/null
+++ b/app/src/main/res/drawable/ic_skip_next_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_skip_previous_white_24dp.xml b/app/src/main/res/drawable/ic_skip_previous_white_24dp.xml
new file mode 100644
index 00000000..487947c7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_skip_previous_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 96d326fc..d1febe89 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -12,4 +12,7 @@
56dp
64dp
+
+ 128dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0b6ac8e5..6631ddde 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -29,7 +29,22 @@
Disable
Ignore
-
- Hello blank fragment
+ Playback
+ Download
+ Couldn\'t play this song.
+ Playlist is empty
+ Audio focus denied.
+
+ Play next
+ Play
+ Play/Pause
+ Previous
+ Next
+ Add to queue
+ Remove from queue
+ Add to playlist
+
+ The playing notification provides actions for play/pause etc.
+ Playing Notification
\ No newline at end of file