From c1c02b3c37f5088f8a04871cff60798d640cdab6 Mon Sep 17 00:00:00 2001 From: Antonio Cappiello Date: Wed, 9 Dec 2020 19:31:35 +0100 Subject: [PATCH] First song played: What she said - The Smiths --- app/src/main/AndroidManifest.xml | 3 +- .../adapter/PlayerNowPlayingSongAdapter.java | 2 +- .../play/adapter/RecentMusicAdapter.java | 3 + .../play/helper/MusicPlayerRemote.java | 337 ++++++++++++++++++ .../helper/MusicProgressViewUpdateHelper.java | 65 ++++ .../interfaces/MusicServiceEventListener.java | 15 + .../com/cappielloantonio/play/model/Song.java | 5 + .../play/service/MusicService.java | 50 +-- .../notification/PlayingNotification.java | 92 ++--- .../play/ui/activities/base/BaseActivity.java | 171 ++++++++- .../fragment/PlayerBottomSheetFragment.java | 123 ++++++- .../layout/item_player_now_playing_song.xml | 5 +- .../res/layout/player_body_bottom_sheet.xml | 23 +- app/src/main/res/values/strings.xml | 2 + 14 files changed, 787 insertions(+), 109 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/play/helper/MusicPlayerRemote.java create mode 100644 app/src/main/java/com/cappielloantonio/play/helper/MusicProgressViewUpdateHelper.java create mode 100644 app/src/main/java/com/cappielloantonio/play/interfaces/MusicServiceEventListener.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 531efac3..cf58d399 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,6 @@ - - + \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/PlayerNowPlayingSongAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerNowPlayingSongAdapter.java index d3095448..6407cc8c 100644 --- a/app/src/main/java/com/cappielloantonio/play/adapter/PlayerNowPlayingSongAdapter.java +++ b/app/src/main/java/com/cappielloantonio/play/adapter/PlayerNowPlayingSongAdapter.java @@ -57,7 +57,7 @@ public class PlayerNowPlayingSongAdapter extends RecyclerView.Adapter mConnectionMap = new WeakHashMap<>(); + + public static ServiceToken bindToService(@NonNull final Context context, final ServiceConnection callback) { + Activity realActivity = ((Activity) context).getParent(); + if (realActivity == null) { + realActivity = (Activity) context; + } + + final ContextWrapper contextWrapper = new ContextWrapper(realActivity); + contextWrapper.startService(new Intent(contextWrapper, MusicService.class)); + + final ServiceBinder binder = new ServiceBinder(callback); + + if (contextWrapper.bindService(new Intent().setClass(contextWrapper, MusicService.class), binder, Context.BIND_AUTO_CREATE)) { + mConnectionMap.put(contextWrapper, binder); + return new ServiceToken(contextWrapper); + } + + return null; + } + + public static void unbindFromService(@Nullable final ServiceToken token) { + if (token == null) { + return; + } + + final ContextWrapper mContextWrapper = token.mWrappedContext; + final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper); + if (mBinder == null) { + return; + } + + mContextWrapper.unbindService(mBinder); + if (mConnectionMap.isEmpty()) { + musicService = null; + } + } + + public static final class ServiceBinder implements ServiceConnection { + private final ServiceConnection mCallback; + + public ServiceBinder(final ServiceConnection callback) { + mCallback = callback; + } + + @Override + public void onServiceConnected(final ComponentName className, final IBinder service) { + MusicService.MusicBinder binder = (MusicService.MusicBinder) service; + musicService = binder.getService(); + if (mCallback != null) { + mCallback.onServiceConnected(className, service); + } + } + + @Override + public void onServiceDisconnected(final ComponentName className) { + if (mCallback != null) { + mCallback.onServiceDisconnected(className); + } + + musicService = null; + } + } + + public static final class ServiceToken { + public ContextWrapper mWrappedContext; + + public ServiceToken(final ContextWrapper context) { + mWrappedContext = context; + } + } + + public static void playSongAt(final int position) { + if (musicService != null) { + musicService.playSongAt(position); + } + } + + public static void setPosition(final int position) { + if (musicService != null) { + musicService.setPosition(position); + } + } + + public static void pauseSong() { + if (musicService != null) { + musicService.pause(); + } + } + + public static void playNextSong() { + if (musicService != null) { + musicService.playNextSong(true); + } + } + + public static void playPreviousSong() { + if (musicService != null) { + musicService.playPreviousSong(true); + } + } + + public static void back() { + if (musicService != null) { + musicService.back(true); + } + } + + public static boolean isPlaying() { + return musicService != null && musicService.isPlaying(); + } + + public static void resumePlaying() { + if (musicService != null) { + musicService.play(); + } + } + + public static void openQueue(final List queue, final int startPosition, final boolean startPlaying) { + if (!tryToHandleOpenPlayingQueue(queue, startPosition, startPlaying) && musicService != null) { + musicService.openQueue(queue, startPosition, startPlaying); + } + } + + public static void openAndShuffleQueue(final List queue, boolean startPlaying) { + int startPosition = 0; + if (!queue.isEmpty()) { + startPosition = new Random().nextInt(queue.size()); + } + + if (!tryToHandleOpenPlayingQueue(queue, startPosition, startPlaying) && musicService != null) { + openQueue(queue, startPosition, startPlaying); + } + } + + private static boolean tryToHandleOpenPlayingQueue(final List queue, final int startPosition, final boolean startPlaying) { + if (getPlayingQueue() == queue) { + if (startPlaying) { + playSongAt(startPosition); + } else { + setPosition(startPosition); + } + + return true; + } + + return false; + } + + public static Song getCurrentSong() { + return musicService.getCurrentSong(); + } + + public static int getPosition() { + if (musicService != null) { + return musicService.getPosition(); + } + + return -1; + } + + public static List getPlayingQueue() { + if (musicService != null) { + return musicService.getPlayingQueue(); + } + + return new ArrayList<>(); + } + + public static int getSongProgressMillis() { + if (musicService != null) { + return musicService.getSongProgressMillis(); + } + + return -1; + } + + public static int getSongDurationMillis() { + if (musicService != null) { + return musicService.getSongDurationMillis(); + } + + return -1; + } + + public static long getQueueDurationMillis(int position) { + if (musicService != null) { + return musicService.getQueueDurationMillis(position); + } + + return -1; + } + + public static int seekTo(int millis) { + if (musicService != null) { + return musicService.seek(millis); + } + + return -1; + } + + public static int getRepeatMode() { + if (musicService != null) { + return musicService.getRepeatMode(); + } + + return MusicService.REPEAT_MODE_NONE; + } + + public static boolean cycleRepeatMode() { + if (musicService != null) { + musicService.cycleRepeatMode(); + return true; + } + + return false; + } + + public static boolean playNext(Song song) { + if (musicService != null) { + if (getPlayingQueue().size() > 0) { + musicService.addSong(getPosition() + 1, song); + } else { + List queue = new ArrayList<>(); + queue.add(song); + openQueue(queue, 0, false); + } + + Toast.makeText(musicService, musicService.getResources().getString(R.string.added_title_to_queue), Toast.LENGTH_SHORT).show(); + return true; + } + + return false; + } + + public static boolean playNext(@NonNull List songs) { + if (musicService != null) { + if (getPlayingQueue().size() > 0) { + musicService.addSongs(getPosition() + 1, songs); + } else { + openQueue(songs, 0, false); + } + + final String toast = songs.size() == 1 ? musicService.getResources().getString(R.string.added_title_to_queue) : musicService.getResources().getString(R.string.added_x_titles_to_queue, songs.size()); + Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show(); + return true; + } + + return false; + } + + public static boolean enqueue(Song song) { + if (musicService != null) { + if (getPlayingQueue().size() > 0) { + musicService.addSong(song); + } else { + List queue = new ArrayList<>(); + queue.add(song); + openQueue(queue, 0, false); + } + + Toast.makeText(musicService, musicService.getResources().getString(R.string.added_title_to_queue), Toast.LENGTH_SHORT).show(); + return true; + } + + return false; + } + + public static boolean enqueue(@NonNull List songs) { + if (musicService != null) { + if (getPlayingQueue().size() > 0) { + musicService.addSongs(songs); + } else { + openQueue(songs, 0, false); + } + + final String toast = songs.size() == 1 ? musicService.getResources().getString(R.string.added_title_to_queue) : musicService.getResources().getString(R.string.added_x_titles_to_queue, songs.size()); + Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show(); + return true; + } + + return false; + } + + public static boolean removeFromQueue(int position) { + if (musicService != null && position >= 0 && position < getPlayingQueue().size()) { + musicService.removeSong(position); + return true; + } + + return false; + } + + public static boolean moveSong(int from, int to) { + if (musicService != null && from >= 0 && to >= 0 && from < getPlayingQueue().size() && to < getPlayingQueue().size()) { + musicService.moveSong(from, to); + return true; + } + + return false; + } + + public static boolean clearQueue() { + if (musicService != null) { + musicService.clearQueue(); + return true; + } + + return false; + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/helper/MusicProgressViewUpdateHelper.java b/app/src/main/java/com/cappielloantonio/play/helper/MusicProgressViewUpdateHelper.java new file mode 100644 index 00000000..1c7dd222 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/helper/MusicProgressViewUpdateHelper.java @@ -0,0 +1,65 @@ +package com.cappielloantonio.play.helper; + +import android.os.Handler; +import android.os.Message; + +import androidx.annotation.NonNull; + +public class MusicProgressViewUpdateHelper extends Handler { + private static final int CMD_REFRESH_PROGRESS_VIEWS = 1; + + private static final int MIN_INTERVAL = 20; + private static final int UPDATE_INTERVAL_PLAYING = 1000; + private static final int UPDATE_INTERVAL_PAUSED = 500; + + private Callback callback; + private int intervalPlaying; + private int intervalPaused; + + public void start() { + queueNextRefresh(1); + } + + public void stop() { + removeMessages(CMD_REFRESH_PROGRESS_VIEWS); + } + + public MusicProgressViewUpdateHelper(Callback callback) { + this.callback = callback; + this.intervalPlaying = UPDATE_INTERVAL_PLAYING; + this.intervalPaused = UPDATE_INTERVAL_PAUSED; + } + + @Override + public void handleMessage(@NonNull Message msg) { + super.handleMessage(msg); + if (msg.what == CMD_REFRESH_PROGRESS_VIEWS) { + queueNextRefresh(refreshProgressViews()); + } + } + + private int refreshProgressViews() { + final int progressMillis = MusicPlayerRemote.getSongProgressMillis(); + final int totalMillis = MusicPlayerRemote.getSongDurationMillis(); + + callback.onUpdateProgressViews(progressMillis, totalMillis); + + if (!MusicPlayerRemote.isPlaying()) { + return intervalPaused; + } + + final int remainingMillis = intervalPlaying - progressMillis % intervalPlaying; + + return Math.max(MIN_INTERVAL, remainingMillis); + } + + private void queueNextRefresh(final long delay) { + final Message message = obtainMessage(CMD_REFRESH_PROGRESS_VIEWS); + removeMessages(CMD_REFRESH_PROGRESS_VIEWS); + sendMessageDelayed(message, delay); + } + + public interface Callback { + void onUpdateProgressViews(int progress, int total); + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/interfaces/MusicServiceEventListener.java b/app/src/main/java/com/cappielloantonio/play/interfaces/MusicServiceEventListener.java new file mode 100644 index 00000000..d750cd14 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/interfaces/MusicServiceEventListener.java @@ -0,0 +1,15 @@ +package com.cappielloantonio.play.interfaces; + +public interface MusicServiceEventListener { + void onServiceConnected(); + + void onServiceDisconnected(); + + void onQueueChanged(); + + void onPlayMetadataChanged(); + + void onPlayStateChanged(); + + void onRepeatModeChanged(); +} diff --git a/app/src/main/java/com/cappielloantonio/play/model/Song.java b/app/src/main/java/com/cappielloantonio/play/model/Song.java index e28f609c..c190c4f5 100644 --- a/app/src/main/java/com/cappielloantonio/play/model/Song.java +++ b/app/src/main/java/com/cappielloantonio/play/model/Song.java @@ -144,6 +144,11 @@ public class Song implements Parcelable { this.lastPlay = lastPlay; } + @Ignore + public Song() { + this.id = UUID.randomUUID().toString(); + } + @Ignore public Song(BaseItemDto itemDto) { this.id = itemDto.getId(); 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 34c978e7..fb151a99 100644 --- a/app/src/main/java/com/cappielloantonio/play/service/MusicService.java +++ b/app/src/main/java/com/cappielloantonio/play/service/MusicService.java @@ -30,6 +30,7 @@ import com.cappielloantonio.play.App; import com.cappielloantonio.play.R; import com.cappielloantonio.play.model.Playlist; import com.cappielloantonio.play.model.Song; +import com.cappielloantonio.play.repository.QueueRepository; import com.cappielloantonio.play.service.notification.PlayingNotification; import com.cappielloantonio.play.service.playback.Playback; import com.cappielloantonio.play.util.PreferenceUtil; @@ -63,14 +64,12 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks 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; @@ -84,9 +83,6 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks 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; @@ -101,7 +97,6 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks private Playback playback; private List playingQueue = new ArrayList<>(); - private List originalPlayingQueue = new ArrayList<>(); private int position = -1; private int nextPosition = -1; @@ -336,12 +331,8 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks } 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); + QueueRepository queueRepository = new QueueRepository(App.getInstance()); + queueRepository.insertAllAndStartNew(playingQueue); } private void savePosition() { @@ -371,14 +362,14 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks private synchronized void restoreQueuesAndPositionIfNecessary() { if (!queuesRestored && playingQueue.isEmpty()) { - List restoredQueue = App.getDatabase().queueSongDao().getQueue(0); - List restoredOriginalQueue = App.getDatabase().queueSongDao().getQueue(1); + QueueRepository queueRepository = new QueueRepository(App.getInstance()); + + List restoredQueue = queueRepository.getSongs(); 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; + if (restoredQueue.size() > 0 && restoredPosition != -1) { this.playingQueue = restoredQueue; position = restoredPosition; @@ -517,7 +508,11 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks } public Song getSongAt(int position) { - return getPlayingQueue().get(position); + if (position >= 0 && position < getPlayingQueue().size()) { + return getPlayingQueue().get(position); + } else { + return new Song(); + } } public int getNextPosition(boolean force) { @@ -577,10 +572,6 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks 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 { @@ -593,30 +584,26 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks 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)); + playingQueue.remove(position); reposition(position); notifyChange(QUEUE_CHANGED); } @@ -653,7 +640,6 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks public void clearQueue() { playingQueue.clear(); - originalPlayingQueue.clear(); setPosition(-1); notifyChange(QUEUE_CHANGED); @@ -1059,8 +1045,8 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks startInfo.setCanSeek(true); startInfo.setIsPaused(false); - App.getInstance().getApiClientInstance(App.getInstance()).ensureWebSocket(); - App.getInstance().getApiClientInstance(App.getInstance()).ReportPlaybackStartAsync(startInfo, new EmptyResponse()); + App.getApiClientInstance(App.getInstance()).ensureWebSocket(); + App.getApiClientInstance(App.getInstance()).ReportPlaybackStartAsync(startInfo, new EmptyResponse()); } public void onProgress() { @@ -1071,10 +1057,10 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks double duration = mService.get().getSongDurationMillis(); if (progress / duration > 0.9) { Song current = mService.get().getCurrentSong(); - String user = App.getInstance().getApiClientInstance(App.getInstance()).getCurrentUserId(); + String user = App.getApiClientInstance(App.getInstance()).getCurrentUserId(); Date time = new Date(System.currentTimeMillis()); - App.getInstance().getApiClientInstance(App.getInstance()).MarkPlayedAsync(current.getId(), user, time, new Response<>()); + App.getApiClientInstance(App.getInstance()).MarkPlayedAsync(current.getId(), user, time, new Response<>()); } progressInfo.setItemId(mService.get().getCurrentSong().getId()); @@ -1083,7 +1069,7 @@ public class MusicService extends Service implements Playback.PlaybackCallbacks progressInfo.setIsPaused(!mService.get().playback.isPlaying()); progressInfo.setCanSeek(true); - App.getInstance().getApiClientInstance(App.getInstance()).ReportPlaybackProgressAsync(progressInfo, new EmptyResponse()); + App.getApiClientInstance(App.getInstance()).ReportPlaybackProgressAsync(progressInfo, new EmptyResponse()); } public void onStop() { 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 index d98fa520..a5342566 100644 --- a/app/src/main/java/com/cappielloantonio/play/service/notification/PlayingNotification.java +++ b/app/src/main/java/com/cappielloantonio/play/service/notification/PlayingNotification.java @@ -9,19 +9,12 @@ 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; @@ -71,64 +64,39 @@ public class PlayingNotification { 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 glideAnimation) { - Palette palette = resource.getPalette(); - update(resource.getBitmap(), palette.getVibrantColor(palette.getMutedColor(Color.TRANSPARENT))); - } + Bitmap 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); - @Override - public void onLoadFailed(Drawable drawable) { - update(null, Color.TRANSPARENT); - } + builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle().setMediaSession(service.getMediaSession().getSessionToken()) + .setShowActionsInCompactView(0, 1, 2)) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setColor(Color.TRANSPARENT); - @Override - public void onLoadCleared(Drawable drawable) { - update(null, Color.TRANSPARENT); - } + // notification has been stopped before loading was finished + if (stopped) return; - 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()); - } - })); + updateNotifyModeAndPostNotification(builder.build()); } public synchronized void stop() { diff --git a/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java b/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java index b4868a95..8a860a89 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java @@ -1,29 +1,80 @@ package com.cappielloantonio.play.ui.activities.base; import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; import android.os.PowerManager; import android.provider.Settings; +import android.util.Log; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import com.cappielloantonio.play.R; +import com.cappielloantonio.play.helper.MusicPlayerRemote; +import com.cappielloantonio.play.interfaces.MusicServiceEventListener; +import com.cappielloantonio.play.service.MusicService; +import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.List; import pub.devrel.easypermissions.AppSettingsDialog; import pub.devrel.easypermissions.EasyPermissions; -public class BaseActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks { +public class BaseActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks, MusicServiceEventListener { + private static final String TAG = "BaseActivity"; public static final int REQUEST_PERM_ACCESS = 1; + private final List mMusicServiceEventListeners = new ArrayList<>(); + + private MusicPlayerRemote.ServiceToken serviceToken; + private MusicStateReceiver musicStateReceiver; + + private boolean receiverRegistered; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "onCreate"); + + serviceToken = MusicPlayerRemote.bindToService(this, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "onServiceConnected"); + BaseActivity.this.onServiceConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d(TAG, "onServiceDisconnected"); + BaseActivity.this.onServiceDisconnected(); + } + }); + } + @Override protected void onResume() { super.onResume(); checkPermissions(); -// checkBatteryOptimization(); + checkBatteryOptimization(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + MusicPlayerRemote.unbindFromService(serviceToken); + if (receiverRegistered) { + unregisterReceiver(musicStateReceiver); + receiverRegistered = false; + } } private void checkBatteryOptimization() { @@ -80,4 +131,120 @@ public class BaseActivity extends AppCompatActivity implements EasyPermissions.P EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); } + + public void addMusicServiceEventListener(final MusicServiceEventListener listener) { + if (listener != null) { + mMusicServiceEventListeners.add(listener); + } + } + + public void removeMusicServiceEventListener(final MusicServiceEventListener listener) { + if (listener != null) { + mMusicServiceEventListeners.remove(listener); + } + } + + @Override + public void onServiceConnected() { + if (!receiverRegistered) { + musicStateReceiver = new MusicStateReceiver(this); + + final IntentFilter filter = new IntentFilter(); + filter.addAction(MusicService.STATE_CHANGED); + filter.addAction(MusicService.REPEAT_MODE_CHANGED); + filter.addAction(MusicService.META_CHANGED); + filter.addAction(MusicService.QUEUE_CHANGED); + + registerReceiver(musicStateReceiver, filter); + + receiverRegistered = true; + } + + for (MusicServiceEventListener listener : mMusicServiceEventListeners) { + if (listener != null) { + listener.onServiceConnected(); + } + } + } + + @Override + public void onServiceDisconnected() { + if (receiverRegistered) { + unregisterReceiver(musicStateReceiver); + receiverRegistered = false; + } + + for (MusicServiceEventListener listener : mMusicServiceEventListeners) { + if (listener != null) { + listener.onServiceDisconnected(); + } + } + } + + @Override + public void onQueueChanged() { + for (MusicServiceEventListener listener : mMusicServiceEventListeners) { + if (listener != null) { + listener.onQueueChanged(); + } + } + } + + @Override + public void onPlayMetadataChanged() { + for (MusicServiceEventListener listener : mMusicServiceEventListeners) { + if (listener != null) { + listener.onPlayMetadataChanged(); + } + } + } + + @Override + public void onPlayStateChanged() { + for (MusicServiceEventListener listener : mMusicServiceEventListeners) { + if (listener != null) { + listener.onPlayStateChanged(); + } + } + } + + @Override + public void onRepeatModeChanged() { + for (MusicServiceEventListener listener : mMusicServiceEventListeners) { + if (listener != null) { + listener.onRepeatModeChanged(); + } + } + } + + private static final class MusicStateReceiver extends BroadcastReceiver { + + private final WeakReference reference; + + public MusicStateReceiver(final BaseActivity activity) { + reference = new WeakReference<>(activity); + } + + @Override + public void onReceive(final Context context, @NonNull final Intent intent) { + final String action = intent.getAction(); + BaseActivity activity = reference.get(); + if (activity != null && action != null) { + switch (action) { + case MusicService.META_CHANGED: + activity.onPlayMetadataChanged(); + break; + case MusicService.QUEUE_CHANGED: + activity.onQueueChanged(); + break; + case MusicService.STATE_CHANGED: + activity.onPlayStateChanged(); + break; + case MusicService.REPEAT_MODE_CHANGED: + activity.onRepeatModeChanged(); + break; + } + } + } + } } 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 2e958ff7..f9aa9303 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,6 +5,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; +import android.widget.SeekBar; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -17,11 +19,14 @@ import com.cappielloantonio.play.R; import com.cappielloantonio.play.adapter.PlayerNowPlayingSongAdapter; import com.cappielloantonio.play.adapter.PlayerSongQueueAdapter; import com.cappielloantonio.play.databinding.FragmentPlayerBottomSheetBinding; +import com.cappielloantonio.play.helper.MusicPlayerRemote; +import com.cappielloantonio.play.helper.MusicProgressViewUpdateHelper; +import com.cappielloantonio.play.interfaces.MusicServiceEventListener; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.ui.activities.MainActivity; import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel; -public class PlayerBottomSheetFragment extends Fragment { +public class PlayerBottomSheetFragment extends Fragment implements MusicServiceEventListener, MusicProgressViewUpdateHelper.Callback { private static final String TAG = "PlayerBottomSheetFragment"; private FragmentPlayerBottomSheetBinding bind; @@ -31,7 +36,14 @@ public class PlayerBottomSheetFragment extends Fragment { private PlayerNowPlayingSongAdapter playerNowPlayingSongAdapter; private PlayerSongQueueAdapter playerSongQueueAdapter; - private boolean isNowPlaying = false; + private MusicProgressViewUpdateHelper progressViewUpdateHelper; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + progressViewUpdateHelper = new MusicProgressViewUpdateHelper(this); + } @Nullable @Override @@ -49,6 +61,35 @@ public class PlayerBottomSheetFragment extends Fragment { return view; } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + activity.addMusicServiceEventListener(this); + setUpMusicControllers(); + } + + @Override + public void onResume() { + super.onResume(); + + progressViewUpdateHelper.start(); + } + + @Override + public void onPause() { + super.onPause(); + + progressViewUpdateHelper.stop(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + activity.removeMusicServiceEventListener(this); + } + private void initQueueSlideView() { bind.playerBodyLayout.playerSongCoverViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); @@ -81,6 +122,28 @@ public class PlayerBottomSheetFragment extends Fragment { bind.playerBodyLayout.buttonFavorite.setOnClickListener(v -> playerBottomSheetViewModel.setFavorite()); } + private void initSeekBar() { + bind.playerBodyLayout.playerBigSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + MusicPlayerRemote.seekTo(progress); + onUpdateProgressViews(MusicPlayerRemote.getSongProgressMillis(), MusicPlayerRemote.getSongDurationMillis()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + } + private void setSongInfo(Song song) { playerBottomSheetViewModel.setNowPlayingSong(song); @@ -95,6 +158,26 @@ public class PlayerBottomSheetFragment extends Fragment { playSong(song); } + private void setUpMusicControllers() { + setUpPlayPauseButton(); +// setUpPrevNext(); +// setUpRepeatButton(); +// setUpShuffleButton(); + initSeekBar(); + } + + private void setUpPlayPauseButton() { + bind.playerBodyLayout.playPauseButton.setOnClickListener(v -> { + if (MusicPlayerRemote.isPlaying()) { + MusicPlayerRemote.pauseSong(); + Toast.makeText(requireContext(), "PAUSING", Toast.LENGTH_SHORT).show(); + } else { + MusicPlayerRemote.resumePlaying(); + Toast.makeText(requireContext(), "PLAYING", Toast.LENGTH_SHORT).show(); + } + }); + } + private void playSong(Song song) { // Toast.makeText(activity, MusicUtil.getSongFileUri(song), Toast.LENGTH_SHORT).show(); } @@ -111,4 +194,40 @@ public class PlayerBottomSheetFragment extends Fragment { bind.playerBodyLayout.playerSongCoverViewPager.setCurrentItem(page, smoothScroll); setSongInfo(song); } + + @Override + public void onServiceConnected() { + + } + + @Override + public void onServiceDisconnected() { + + } + + @Override + public void onQueueChanged() { + + } + + @Override + public void onPlayMetadataChanged() { + + } + + @Override + public void onPlayStateChanged() { + + } + + @Override + public void onRepeatModeChanged() { + + } + + @Override + public void onUpdateProgressViews(int progress, int total) { + bind.playerBodyLayout.playerBigSeekBar.setMax(total); + bind.playerBodyLayout.playerBigSeekBar.setProgress(progress); + } } diff --git a/app/src/main/res/layout/item_player_now_playing_song.xml b/app/src/main/res/layout/item_player_now_playing_song.xml index 54d28e6d..4cb60b3c 100644 --- a/app/src/main/res/layout/item_player_now_playing_song.xml +++ b/app/src/main/res/layout/item_player_now_playing_song.xml @@ -10,9 +10,8 @@ app:cardCornerRadius="4dp"> + android:layout_height="match_parent"/> \ No newline at end of file diff --git a/app/src/main/res/layout/player_body_bottom_sheet.xml b/app/src/main/res/layout/player_body_bottom_sheet.xml index b66e2c7f..b5b6cf9e 100644 --- a/app/src/main/res/layout/player_body_bottom_sheet.xml +++ b/app/src/main/res/layout/player_body_bottom_sheet.xml @@ -22,6 +22,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + - @@ -51,7 +64,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintTop_toBottomOf="@+id/player_big_progress_bar" + app:layout_constraintTop_toBottomOf="@+id/player_big_seek_bar" app:layout_constraintEnd_toEndOf="parent" android:orientation="horizontal"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6631ddde..46181001 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,8 @@ Couldn\'t play this song. Playlist is empty Audio focus denied. + "Added 1 title to the queue." + Added %1$d titles to the queue. Play next Play