diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt b/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt index 2ca4e444e..d5ee33294 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt @@ -371,6 +371,7 @@ object MusicPlayerRemote { return false } + @JvmStatic fun removeFromQueue(song: Song): Boolean { if (musicService != null) { musicService!!.removeSong(song) diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/SongLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/SongLoader.kt index c75b8f0fd..e03b6ef7b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/SongLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/SongLoader.kt @@ -74,6 +74,7 @@ object SongLoader { return song } + @JvmStatic fun getSong(context: Context, queryId: Int): Song { val cursor = makeSongCursor(context, AudioColumns._ID + "=?", arrayOf(queryId.toString())) return getSong(cursor) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MediaStoreObserver.kt b/app/src/main/java/code/name/monkey/retromusic/service/MediaStoreObserver.kt index 131154ef6..5aad0f000 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MediaStoreObserver.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MediaStoreObserver.kt @@ -38,6 +38,6 @@ class MediaStoreObserver( companion object { // milliseconds to delay before calling refresh to aggregate events - private val REFRESH_DELAY: Long = 500 + private const val REFRESH_DELAY: Long = 500 } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java index 36d351619..0bf2102c5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java @@ -44,21 +44,9 @@ import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; - -import com.bumptech.glide.BitmapRequestBuilder; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.animation.GlideAnimation; -import com.bumptech.glide.request.target.SimpleTarget; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Random; - import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.appwidgets.AppWidgetBig; import code.name.monkey.retromusic.appwidgets.AppWidgetCard; @@ -80,71 +68,122 @@ import code.name.monkey.retromusic.service.playback.Playback; import code.name.monkey.retromusic.util.MusicUtil; import code.name.monkey.retromusic.util.PreferenceUtil; import code.name.monkey.retromusic.util.RetroUtil; +import com.bumptech.glide.BitmapRequestBuilder; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.request.target.SimpleTarget; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Random; /** * @author Karim Abou Zeid (kabouzeid), Andrew Neal */ public class MusicService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks { + + public class MusicBinder extends Binder { + + @NonNull + public MusicService getService() { + return MusicService.this; + } + } + public static final String TAG = MusicService.class.getSimpleName(); public static final String RETRO_MUSIC_PACKAGE_NAME = "code.name.monkey.retromusic"; + public static final String MUSIC_PACKAGE_NAME = "com.android.music"; public static final String ACTION_TOGGLE_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".togglepause"; + public static final String ACTION_PLAY = RETRO_MUSIC_PACKAGE_NAME + ".play"; + public static final String ACTION_PLAY_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + ".play.playlist"; + public static final String ACTION_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".pause"; + public static final String ACTION_STOP = RETRO_MUSIC_PACKAGE_NAME + ".stop"; + public static final String ACTION_SKIP = RETRO_MUSIC_PACKAGE_NAME + ".skip"; + public static final String ACTION_REWIND = RETRO_MUSIC_PACKAGE_NAME + ".rewind"; + public static final String ACTION_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".quitservice"; + public static final String ACTION_PENDING_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".pendingquitservice"; + public static final String INTENT_EXTRA_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + "intentextra.playlist"; + public static final String INTENT_EXTRA_SHUFFLE_MODE = RETRO_MUSIC_PACKAGE_NAME + ".intentextra.shufflemode"; public static final String APP_WIDGET_UPDATE = RETRO_MUSIC_PACKAGE_NAME + ".appwidgetupdate"; + public static final String EXTRA_APP_WIDGET_NAME = RETRO_MUSIC_PACKAGE_NAME + "app_widget_name"; // Do not change these three strings as it will break support with other apps (e.g. last.fm scrobbling) public static final String META_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".metachanged"; + public static final String QUEUE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".queuechanged"; + public static final String PLAY_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".playstatechanged"; public static final String FAVORITE_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + "favoritestatechanged"; public static final String REPEAT_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".repeatmodechanged"; + public static final String SHUFFLE_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".shufflemodechanged"; + public static final String MEDIA_STORE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".mediastorechanged"; public static final String CYCLE_REPEAT = RETRO_MUSIC_PACKAGE_NAME + ".cyclerepeat"; + public static final String TOGGLE_SHUFFLE = RETRO_MUSIC_PACKAGE_NAME + ".toggleshuffle"; + public static final String TOGGLE_FAVORITE = RETRO_MUSIC_PACKAGE_NAME + ".togglefavorite"; public static final String SAVED_POSITION = "POSITION"; + public static final String SAVED_POSITION_IN_TRACK = "POSITION_IN_TRACK"; + public static final String SAVED_SHUFFLE_MODE = "SHUFFLE_MODE"; + public static final String SAVED_REPEAT_MODE = "REPEAT_MODE"; public static final int RELEASE_WAKELOCK = 0; + public static final int TRACK_ENDED = 1; + public static final int TRACK_WENT_TO_NEXT = 2; + 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 RESTORE_QUEUES = 9; 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_QUEUES = 0; + private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE @@ -152,18 +191,140 @@ public class MusicService extends Service implements | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SEEK_TO; - private final IBinder musicBind = new MusicBinder(); + + public int nextPosition = -1; + public boolean pendingQuit = false; @Nullable public Playback playback; + public int position = -1; - public int nextPosition = -1; + private AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance(); - private AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance(); - private AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance(); + private AppWidgetCard appWidgetCard = AppWidgetCard.Companion.getInstance(); + + private AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance(); + + private AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance(); + private AppWidgetText appWidgetText = AppWidgetText.Companion.getInstance(); + + private AudioManager audioManager; + + private IntentFilter becomingNoisyReceiverIntentFilter = new IntentFilter( + AudioManager.ACTION_AUDIO_BECOMING_NOISY); + + private boolean becomingNoisyReceiverRegistered; + + private IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + + private boolean headsetReceiverRegistered = false; + + private MediaSessionCompat mediaSession; + + private ContentObserver mediaStoreObserver; + + private final IBinder musicBind = new MusicBinder(); + + private HandlerThread musicPlayerHandlerThread; + + private boolean notHandledMetaChangedForCurrentTrack; + + private ArrayList originalPlayingQueue = new ArrayList<>(); + + private boolean pausedByTransientLossOfFocus; + + private final BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, @NonNull Intent intent) { + if (intent.getAction() != null && intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { + pause(); + } + } + }; + + private PlaybackHandler playerHandler; + + private final AudioManager.OnAudioFocusChangeListener audioFocusListener + = new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(final int focusChange) { + playerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget(); + } + }; + + private PlayingNotification playingNotification; + + private ArrayList playingQueue = new ArrayList<>(); + + private QueueSaveHandler queueSaveHandler; + + private HandlerThread queueSaveHandlerThread; + + private boolean queuesRestored; + + private int repeatMode; + + private int shuffleMode; + + private SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper(); + + private PhoneStateListener phoneStateListener = new PhoneStateListener() { + @Override + public void onCallStateChanged(int state, String incomingNumber) { + switch (state) { + case TelephonyManager.CALL_STATE_IDLE: + //Not in call: Play music + play(); + break; + case TelephonyManager.CALL_STATE_RINGING: + case TelephonyManager.CALL_STATE_OFFHOOK: + //A call is dialing, active or on hold + pause(); + break; + default: + } + super.onCallStateChanged(state, incomingNumber); + } + }; + + private BroadcastReceiver headsetReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action != null) { + if (Intent.ACTION_HEADSET_PLUG.equals(action)) { + int state = intent.getIntExtra("state", -1); + switch (state) { + case 0: + Log.d(TAG, "Headset unplugged"); + pause(); + break; + case 1: + Log.d(TAG, "Headset plugged"); + play(); + break; + } + } + } + } + }; + + private ThrottledSeekHandler throttledSeekHandler; + + private Handler uiThreadHandler; + + private final BroadcastReceiver updateFavoriteReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + updateNotification(); + } + }; + + private PowerManager.WakeLock wakeLock; + private final BroadcastReceiver widgetIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { @@ -196,105 +357,6 @@ public class MusicService extends Service implements } }; - private ArrayList playingQueue = new ArrayList<>(); - private ArrayList originalPlayingQueue = new ArrayList<>(); - private int shuffleMode; - private int repeatMode; - private boolean queuesRestored; - private boolean pausedByTransientLossOfFocus; - private final BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, @NonNull Intent intent) { - if (intent.getAction() != null && intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { - pause(); - } - } - }; - private PlayingNotification playingNotification; - private final BroadcastReceiver updateFavoriteReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - updateNotification(); - } - }; - private AudioManager audioManager; - private MediaSessionCompat mediaSession; - private PowerManager.WakeLock wakeLock; - private PlaybackHandler playerHandler; - private final AudioManager.OnAudioFocusChangeListener audioFocusListener = new AudioManager.OnAudioFocusChangeListener() { - @Override - public void onAudioFocusChange(final int focusChange) { - playerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget(); - } - }; - private QueueSaveHandler queueSaveHandler; - private HandlerThread musicPlayerHandlerThread; - private HandlerThread queueSaveHandlerThread; - private SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper(); - private ThrottledSeekHandler throttledSeekHandler; - private boolean becomingNoisyReceiverRegistered; - private IntentFilter becomingNoisyReceiverIntentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); - private ContentObserver mediaStoreObserver; - private boolean notHandledMetaChangedForCurrentTrack; - private PhoneStateListener phoneStateListener = new PhoneStateListener() { - @Override - public void onCallStateChanged(int state, String incomingNumber) { - switch (state) { - case TelephonyManager.CALL_STATE_IDLE: - //Not in call: Play music - play(); - break; - case TelephonyManager.CALL_STATE_RINGING: - case TelephonyManager.CALL_STATE_OFFHOOK: - //A call is dialing, active or on hold - pause(); - break; - default: - } - super.onCallStateChanged(state, incomingNumber); - } - }; - private Handler uiThreadHandler; - private IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); - private boolean headsetReceiverRegistered = false; - private BroadcastReceiver headsetReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action != null) { - if (Intent.ACTION_HEADSET_PLUG.equals(action)) { - int state = intent.getIntExtra("state", -1); - switch (state) { - case 0: - Log.d(TAG, "Headset unplugged"); - pause(); - break; - case 1: - Log.d(TAG, "Headset plugged"); - play(); - break; - } - } - } - } - }; - - private static String getTrackUri(@NonNull Song song) { - return MusicUtil.getSongFileUri(song.getId()).toString(); - } - - private static Bitmap copy(Bitmap bitmap) { - Bitmap.Config config = bitmap.getConfig(); - if (config == null) { - config = Bitmap.Config.RGB_565; - } - try { - return bitmap.copy(config, false); - } catch (OutOfMemoryError e) { - e.printStackTrace(); - return null; - } - } @Override public void onCreate() { @@ -334,10 +396,18 @@ public class MusicService extends Service implements mediaStoreObserver = new MediaStoreObserver(this, playerHandler); throttledSeekHandler = new ThrottledSeekHandler(this, playerHandler); - getContentResolver().registerContentObserver( - MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver().registerContentObserver( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + + getContentResolver().registerContentObserver(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Albums.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Artists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Genres.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver().registerContentObserver(MediaStore.Audio.Playlists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + PreferenceUtil.getInstance(this).registerOnSharedPreferenceChangedListener(this); @@ -348,41 +418,355 @@ public class MusicService extends Service implements registerHeadsetEvents(); } - private AudioManager getAudioManager() { - if (audioManager == null) { - audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + @Override + public void onDestroy() { + unregisterReceiver(widgetIntentReceiver); + unregisterReceiver(updateFavoriteReceiver); + if (becomingNoisyReceiverRegistered) { + unregisterReceiver(becomingNoisyReceiver); + becomingNoisyReceiverRegistered = false; } - return audioManager; + if (headsetReceiverRegistered) { + unregisterReceiver(headsetReceiver); + headsetReceiverRegistered = false; + } + mediaSession.setActive(false); + quit(); + releaseResources(); + getContentResolver().unregisterContentObserver(mediaStoreObserver); + PreferenceUtil.getInstance(this).unregisterOnSharedPreferenceChangedListener(this); + wakeLock.release(); + + sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_DESTROYED")); } - private void setupMediaSession() { - ComponentName mediaButtonReceiverComponentName = new ComponentName( - getApplicationContext(), - MediaButtonIntentReceiver.class); + public void acquireWakeLock(long milli) { + wakeLock.acquire(milli); + } - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(mediaButtonReceiverComponentName); + 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); + } - PendingIntent mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast( - getApplicationContext(), - 0, - mediaButtonIntent, - 0); + public void addSongs(int position, List songs) { + playingQueue.addAll(position, songs); + originalPlayingQueue.addAll(position, songs); + notifyChange(QUEUE_CHANGED); + } - mediaSession = new MediaSessionCompat(this, - "RetroMusicPlayer", - mediaButtonReceiverComponentName, - mediaButtonReceiverPendingIntent); - MediaSessionCallback mediasessionCallback = new MediaSessionCallback( - getApplicationContext(), this); - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS - ); - mediaSession.setCallback(mediasessionCallback); - mediaSession.setActive(true); - mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent); + public void addSongs(List songs) { + playingQueue.addAll(songs); + originalPlayingQueue.addAll(songs); + notifyChange(QUEUE_CHANGED); + } + public void back(boolean force) { + if (getSongProgressMillis() > 2000) { + seek(0); + } else { + playPreviousSong(force); + } + } + + public void clearQueue() { + playingQueue.clear(); + originalPlayingQueue.clear(); + + setPosition(-1); + notifyChange(QUEUE_CHANGED); + } + + 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; + } + } + + public int getAudioSessionId() { + if (playback != null) { + return playback.getAudioSessionId(); + } + return -1; + } + + @NonNull + public Song getCurrentSong() { + return getSongAt(getPosition()); + } + + @NonNull + public MediaSessionCompat getMediaSession() { + return mediaSession; + } + + 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; + } + + @Nullable + public ArrayList getPlayingQueue() { + return playingQueue; + } + + public int getPosition() { + return position; + } + + 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(); + } + + public int getPreviousPosition(boolean force) { + int newPosition = getPosition() - 1; + switch (repeatMode) { + case REPEAT_MODE_ALL: + if (newPosition < 0) { + if (getPlayingQueue() != null) { + newPosition = getPlayingQueue().size() - 1; + } + } + break; + case REPEAT_MODE_THIS: + if (force) { + if (newPosition < 0) { + if (getPlayingQueue() != null) { + newPosition = getPlayingQueue().size() - 1; + } + } + } else { + newPosition = getPosition(); + } + break; + default: + case REPEAT_MODE_NONE: + if (newPosition < 0) { + newPosition = 0; + } + break; + } + return newPosition; + } + + 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 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(SAVED_REPEAT_MODE, repeatMode) + .apply(); + prepareNext(); + handleAndSendChangeInternal(REPEAT_MODE_CHANGED); + break; + } + } + + public int getShuffleMode() { + return shuffleMode; + } + + public void setShuffleMode(final int shuffleMode) { + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putInt(SAVED_SHUFFLE_MODE, shuffleMode) + .apply(); + switch (shuffleMode) { + case SHUFFLE_MODE_SHUFFLE: + this.shuffleMode = shuffleMode; + if (this.getPlayingQueue() != null) { + ShuffleHelper.INSTANCE.makeShuffleList(this.getPlayingQueue(), getPosition()); + } + position = 0; + break; + case SHUFFLE_MODE_NONE: + this.shuffleMode = shuffleMode; + int currentSongId = Objects.requireNonNull(getCurrentSong()).getId(); + playingQueue = new ArrayList<>(originalPlayingQueue); + int newPosition = 0; + if (getPlayingQueue() != null) { + for (Song song : getPlayingQueue()) { + if (song.getId() == currentSongId) { + newPosition = getPlayingQueue().indexOf(song); + } + } + } + position = newPosition; + break; + } + handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); + notifyChange(QUEUE_CHANGED); + } + + @NonNull + public Song getSongAt(int position) { + if (position >= 0 && getPlayingQueue() != null && position < getPlayingQueue().size()) { + return getPlayingQueue().get(position); + } else { + return Song.Companion.getEmptySong(); + } + } + + public int getSongDurationMillis() { + if (playback != null) { + return playback.duration(); + } + return -1; + } + + public int getSongProgressMillis() { + if (playback != null) { + return playback.position(); + } + return -1; + } + + public void handleAndSendChangeInternal(@NonNull final String what) { + handleChangeInternal(what); + sendChangeInternal(what); + } + + public void initNotification() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PreferenceUtil.getInstance(this) + .classicNotification()) { + playingNotification = new PlayingNotificationImpl(); + } else { + playingNotification = new PlayingNotificationOreo(); + } + playingNotification.init(this); + } + + public boolean isLastTrack() { + if (getPlayingQueue() != null) { + return getPosition() == getPlayingQueue().size() - 1; + } + return false; + } + + public boolean isPausedByTransientLossOfFocus() { + return pausedByTransientLossOfFocus; + } + + public void setPausedByTransientLossOfFocus(boolean pausedByTransientLossOfFocus) { + this.pausedByTransientLossOfFocus = pausedByTransientLossOfFocus; + } + + public boolean isPlaying() { + return playback != null && playback.isPlaying(); + } + + 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 (getShuffleMode() == SHUFFLE_MODE_NONE) { + Song tmpSong = originalPlayingQueue.remove(from); + originalPlayingQueue.add(to, tmpSong); + } + 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 notifyChange(@NonNull final String what) { + handleAndSendChangeInternal(what); + sendPublicIntent(what); + } + + @NonNull + @Override + public IBinder onBind(Intent intent) { + return musicBind; + } + + @Override + public void onSharedPreferenceChanged(@NonNull SharedPreferences sharedPreferences, @NonNull String key) { + switch (key) { + case PreferenceUtil.GAPLESS_PLAYBACK: + if (sharedPreferences.getBoolean(key, false)) { + prepareNext(); + } else { + if (playback != null) { + playback.setNextDataSource(null); + } + } + break; + case PreferenceUtil.ALBUM_ART_ON_LOCKSCREEN: + case PreferenceUtil.BLURRED_ALBUM_ART: + updateMediaSessionMetaData(); + break; + case PreferenceUtil.COLORED_NOTIFICATION: + case PreferenceUtil.DOMINANT_COLOR: + updateNotification(); + break; + case PreferenceUtil.CLASSIC_NOTIFICATION: + initNotification(); + updateNotification(); + break; + case PreferenceUtil.TOGGLE_HEADSET: + registerHeadsetEvents(); + break; + } } @Override @@ -432,53 +816,15 @@ public class MusicService extends Service implements return START_NOT_STICKY; } - private void playFromPlaylist(Intent intent) { - Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); - int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode()); - if (playlist != null) { - ArrayList playlistSongs = playlist.getSongs(getApplicationContext()); - if (!playlistSongs.isEmpty()) { - if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { - int startPosition = new Random().nextInt(playlistSongs.size()); - openQueue(playlistSongs, startPosition, true); - setShuffleMode(shuffleMode); - } else { - 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(); - } + @Override + public void onTrackEnded() { + acquireWakeLock(30000); + playerHandler.sendEmptyMessage(TRACK_ENDED); } @Override - public void onDestroy() { - unregisterReceiver(widgetIntentReceiver); - unregisterReceiver(updateFavoriteReceiver); - if (becomingNoisyReceiverRegistered) { - unregisterReceiver(becomingNoisyReceiver); - becomingNoisyReceiverRegistered = false; - } - if (headsetReceiverRegistered) { - unregisterReceiver(headsetReceiver); - headsetReceiverRegistered = false; - } - mediaSession.setActive(false); - quit(); - releaseResources(); - getContentResolver().unregisterContentObserver(mediaStoreObserver); - PreferenceUtil.getInstance(this).unregisterOnSharedPreferenceChangedListener(this); - wakeLock.release(); - - sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_DESTROYED")); - } - - @NonNull - @Override - public IBinder onBind(Intent intent) { - return musicBind; + public void onTrackWentToNext() { + playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT); } @Override @@ -489,338 +835,10 @@ public class MusicService extends Service implements return true; } - public void saveQueuesImpl() { - MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue); - } - - private void savePosition() { - PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(SAVED_POSITION, getPosition()).apply(); - } - - public void savePositionInTrack() { - PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(SAVED_POSITION_IN_TRACK, getSongProgressMillis()).apply(); - } - - public void saveState() { - saveQueues(); - savePosition(); - savePositionInTrack(); - } - - private void saveQueues() { - queueSaveHandler.removeMessages(SAVE_QUEUES); - queueSaveHandler.sendEmptyMessage(SAVE_QUEUES); - } - - private void restoreState() { - shuffleMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_SHUFFLE_MODE, 0); - repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_REPEAT_MODE, 0); - handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); - handleAndSendChangeInternal(REPEAT_MODE_CHANGED); - - playerHandler.removeMessages(RESTORE_QUEUES); - playerHandler.sendEmptyMessage(RESTORE_QUEUES); - } - - public synchronized void restoreQueuesAndPositionIfNecessary() { - if (!queuesRestored && playingQueue.isEmpty()) { - ArrayList restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue(); - ArrayList restoredOriginalQueue = MusicPlaybackQueueStore.getInstance(this).getSavedOriginalPlayingQueue(); - int restoredPosition = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION, -1); - int restoredPositionInTrack = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION_IN_TRACK, -1); - - if (restoredQueue.size() > 0 && restoredQueue.size() == restoredOriginalQueue.size() && restoredPosition != -1) { - this.originalPlayingQueue = restoredOriginalQueue; - this.playingQueue = restoredQueue; - - position = restoredPosition; - openCurrent(); - prepareNext(); - - if (restoredPositionInTrack > 0) seek(restoredPositionInTrack); - - notHandledMetaChangedForCurrentTrack = true; - sendChangeInternal(META_CHANGED); - sendChangeInternal(QUEUE_CHANGED); - } - } - queuesRestored = true; - } - - public void quit() { - pause(); - playingNotification.stop(); - - closeAudioEffectSession(); - getAudioManager().abandonAudioFocus(audioFocusListener); - stopSelf(); - } - - - private void releaseResources() { - playerHandler.removeCallbacksAndMessages(null); - musicPlayerHandlerThread.quitSafely(); - queueSaveHandler.removeCallbacksAndMessages(null); - queueSaveHandlerThread.quitSafely(); - if (playback != null) { - playback.release(); - } - playback = null; - mediaSession.release(); - } - - public boolean isPlaying() { - return playback != null && playback.isPlaying(); - } - - public int getPosition() { - return position; - } - - 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(); - } - - public void playNextSong(boolean force) { - playSongAt(getNextPosition(force)); - } - - public boolean openTrackAndPrepareNextAt(int position) { - synchronized (this) { - this.position = position; - boolean prepared = openCurrent(); - if (prepared) prepareNextImpl(); - notifyChange(META_CHANGED); - notHandledMetaChangedForCurrentTrack = false; - return prepared; - } - } - - private boolean openCurrent() { - synchronized (this) { - try { - if (playback != null) { - return playback.setDataSource(getTrackUri(Objects.requireNonNull(getCurrentSong()))); - } - } catch (Exception e) { - return false; - } - } - return false; - } - - private void prepareNext() { - playerHandler.removeMessages(PREPARE_NEXT); - playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget(); - } - - public boolean prepareNextImpl() { - synchronized (this) { - try { - int nextPosition = getNextPosition(false); - if (playback != null) { - playback.setNextDataSource(getTrackUri(Objects.requireNonNull(getSongAt(nextPosition)))); - } - this.nextPosition = nextPosition; - return true; - } catch (Exception e) { - return false; - } - } - } - - private void closeAudioEffectSession() { - final Intent audioEffectsIntent = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); - if (playback != null) { - audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playback.getAudioSessionId()); - } - audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); - sendBroadcast(audioEffectsIntent); - } - - private boolean requestFocus() { - return (getAudioManager().requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - } - - public void initNotification() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PreferenceUtil.getInstance(this).classicNotification()) { - playingNotification = new PlayingNotificationImpl(); - } else { - playingNotification = new PlayingNotificationOreo(); - } - playingNotification.init(this); - } - - public void updateNotification() { - if (playingNotification != null && getCurrentSong().getId() != -1) { - playingNotification.update(); - } - } - - public void updateMediaSessionPlaybackState() { - PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder() - .setActions(MEDIA_SESSION_ACTIONS) - .setState(isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, - getSongProgressMillis(), 1); - - setCustomAction(stateBuilder); - - mediaSession.setPlaybackState(stateBuilder.build()); - } - - private void setCustomAction(PlaybackStateCompat.Builder stateBuilder) { - int repeatIcon = R.drawable.ic_repeat_white_24dp; // REPEAT_MODE_NONE - if (getRepeatMode() == REPEAT_MODE_THIS) { - repeatIcon = R.drawable.ic_repeat_one_white_24dp; - } else if (getRepeatMode() == REPEAT_MODE_ALL) { - repeatIcon = R.drawable.ic_repeat_white_circle_24dp; - } - stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( - CYCLE_REPEAT, getString(R.string.action_cycle_repeat), repeatIcon) - .build()); - - final int shuffleIcon = getShuffleMode() == SHUFFLE_MODE_NONE ? R.drawable.ic_shuffle_off_circled : R.drawable.ic_shuffle_on_circled; - stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( - TOGGLE_SHUFFLE, getString(R.string.action_toggle_shuffle), shuffleIcon) - .build()); - - final int favoriteIcon = MusicUtil.isFavorite(getApplicationContext(), getCurrentSong()) ? R.drawable.ic_favorite_white_24dp : R.drawable.ic_favorite_border_white_24dp; - stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( - TOGGLE_FAVORITE, getString(R.string.action_toggle_favorite), favoriteIcon) - .build()); - } - - void updateMediaSessionMetaData() { - final Song song = getCurrentSong(); - - if (song.getId() == -1) { - 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) - .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getPlayingQueue().size()); - - - if (PreferenceUtil.getInstance(this).albumArtOnLockscreen()) { - final Point screenSize = RetroUtil.getScreenSize(MusicService.this); - final BitmapRequestBuilder request = SongGlideRequest.Builder.from(Glide.with(MusicService.this), song) - .checkIgnoreMediaStore(MusicService.this) - .asBitmap().build(); - if (PreferenceUtil.getInstance(this).blurredAlbumArt()) { - request.transform(new BlurTransformation.Builder(MusicService.this).build()); - } - runOnUiThread(new Runnable() { - @Override - public void run() { - request.into(new SimpleTarget(screenSize.x, screenSize.y) { - @Override - public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { - metaData.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, copy(resource)); - mediaSession.setMetadata(metaData.build()); - } - - @Override - public void onLoadFailed(Exception e, Drawable errorDrawable) { - super.onLoadFailed(e, errorDrawable); - mediaSession.setMetadata(metaData.build()); - } - }); - } - }); - } else { - mediaSession.setMetadata(metaData.build()); - } - } - - public void runOnUiThread(Runnable runnable) { - uiThreadHandler.post(runnable); - } - - @NonNull - public Song getCurrentSong() { - return getSongAt(getPosition()); - } - - @NonNull - public Song getSongAt(int position) { - if (position >= 0 && getPlayingQueue() != null && position < getPlayingQueue().size()) { - return getPlayingQueue().get(position); - } else { - return Song.Companion.getEmptySong(); - } - } - - 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; - } - - public boolean isLastTrack() { - if (getPlayingQueue() != null) { - return getPosition() == getPlayingQueue().size() - 1; - } - return false; - } - - @Nullable - public ArrayList 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(SAVED_REPEAT_MODE, repeatMode) - .apply(); - prepareNext(); - handleAndSendChangeInternal(REPEAT_MODE_CHANGED); - break; - } - } - - public void openQueue(@Nullable final ArrayList playingQueue, final int startPosition, final boolean startPlaying) { - if (playingQueue != null && !playingQueue.isEmpty() && startPosition >= 0 && startPosition < playingQueue.size()) { + public void openQueue(@Nullable final ArrayList 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/remove songs later originalPlayingQueue = new ArrayList<>(playingQueue); this.playingQueue = new ArrayList<>(originalPlayingQueue); @@ -839,109 +857,16 @@ public class MusicService extends Service implements } } - 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) { - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - playingQueue.remove(position); - originalPlayingQueue.remove(position); - } else { - originalPlayingQueue.remove(playingQueue.remove(position)); - } - - rePosition(position); - - notifyChange(QUEUE_CHANGED); - } - - public void removeSong(@NonNull Song song) { - for (int i = 0; i < playingQueue.size(); i++) { - if (playingQueue.get(i).getId() == song.getId()) { - playingQueue.remove(i); - rePosition(i); + public boolean openTrackAndPrepareNextAt(int position) { + synchronized (this) { + this.position = position; + boolean prepared = openCurrent(); + if (prepared) { + prepareNextImpl(); } - } - for (int i = 0; i < originalPlayingQueue.size(); i++) { - if (originalPlayingQueue.get(i).getId() == song.getId()) { - originalPlayingQueue.remove(i); - } - } - 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 (getShuffleMode() == SHUFFLE_MODE_NONE) { - Song tmpSong = originalPlayingQueue.remove(from); - originalPlayingQueue.add(to, tmpSong); - } - 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 playSongAtImpl(int position) { - if (openTrackAndPrepareNextAt(position)) { - play(); - } else { - Toast.makeText(this, getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); + notifyChange(META_CHANGED); + notHandledMetaChangedForCurrentTrack = false; + return prepared; } } @@ -977,11 +902,34 @@ public class MusicService extends Service implements } } } else { - Toast.makeText(this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT).show(); + Toast.makeText(this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT) + .show(); } } } + public void playNextSong(boolean force) { + playSongAt(getNextPosition(force)); + } + + public void playPreviousSong(boolean force) { + playSongAt(getPreviousPosition(force)); + } + + 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 playSongAtImpl(int position) { + if (openTrackAndPrepareNextAt(position)) { + play(); + } else { + Toast.makeText(this, getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); + } + } + public void playSongs(ArrayList songs, int shuffleMode) { if (songs != null && !songs.isEmpty()) { if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { @@ -997,68 +945,111 @@ public class MusicService extends Service implements } } - public void playPreviousSong(boolean force) { - playSongAt(getPreviousPosition(force)); + public boolean prepareNextImpl() { + synchronized (this) { + try { + int nextPosition = getNextPosition(false); + if (playback != null) { + playback.setNextDataSource(getTrackUri(Objects.requireNonNull(getSongAt(nextPosition)))); + } + this.nextPosition = nextPosition; + return true; + } catch (Exception e) { + return false; + } + } } - public void back(boolean force) { - if (getSongProgressMillis() > 2000) { - seek(0); + public void quit() { + pause(); + playingNotification.stop(); + + closeAudioEffectSession(); + getAudioManager().abandonAudioFocus(audioFocusListener); + stopSelf(); + } + + public void releaseWakeLock() { + if (wakeLock.isHeld()) { + wakeLock.release(); + } + } + + public void removeSong(int position) { + if (getShuffleMode() == SHUFFLE_MODE_NONE) { + playingQueue.remove(position); + originalPlayingQueue.remove(position); } else { - playPreviousSong(force); + originalPlayingQueue.remove(playingQueue.remove(position)); } + + rePosition(position); + + notifyChange(QUEUE_CHANGED); } - public int getPreviousPosition(boolean force) { - int newPosition = getPosition() - 1; - switch (repeatMode) { - case REPEAT_MODE_ALL: - if (newPosition < 0) { - if (getPlayingQueue() != null) { - newPosition = getPlayingQueue().size() - 1; - } + public void removeSong(@NonNull Song song) { + for (int i = 0; i < playingQueue.size(); i++) { + if (playingQueue.get(i).getId() == song.getId()) { + playingQueue.remove(i); + rePosition(i); + } + } + for (int i = 0; i < originalPlayingQueue.size(); i++) { + if (originalPlayingQueue.get(i).getId() == song.getId()) { + originalPlayingQueue.remove(i); + } + } + notifyChange(QUEUE_CHANGED); + } + + public synchronized void restoreQueuesAndPositionIfNecessary() { + if (!queuesRestored && playingQueue.isEmpty()) { + ArrayList restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue(); + ArrayList restoredOriginalQueue = MusicPlaybackQueueStore.getInstance(this) + .getSavedOriginalPlayingQueue(); + int restoredPosition = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION, -1); + int restoredPositionInTrack = PreferenceManager.getDefaultSharedPreferences(this) + .getInt(SAVED_POSITION_IN_TRACK, -1); + + if (restoredQueue.size() > 0 && restoredQueue.size() == restoredOriginalQueue.size() + && restoredPosition != -1) { + this.originalPlayingQueue = restoredOriginalQueue; + this.playingQueue = restoredQueue; + + position = restoredPosition; + openCurrent(); + prepareNext(); + + if (restoredPositionInTrack > 0) { + seek(restoredPositionInTrack); } - break; - case REPEAT_MODE_THIS: - if (force) { - if (newPosition < 0) { - if (getPlayingQueue() != null) { - newPosition = getPlayingQueue().size() - 1; - } - } - } else { - newPosition = getPosition(); - } - break; - default: - case REPEAT_MODE_NONE: - if (newPosition < 0) { - newPosition = 0; - } - break; + + notHandledMetaChangedForCurrentTrack = true; + sendChangeInternal(META_CHANGED); + sendChangeInternal(QUEUE_CHANGED); + } } - return newPosition; + queuesRestored = true; } - public int getSongProgressMillis() { - if (playback != null) { - return playback.position(); - } - return -1; + public void runOnUiThread(Runnable runnable) { + uiThreadHandler.post(runnable); } - public int getSongDurationMillis() { - if (playback != null) { - return playback.duration(); - } - return -1; + public void savePositionInTrack() { + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putInt(SAVED_POSITION_IN_TRACK, getSongProgressMillis()).apply(); } - 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 void saveQueuesImpl() { + MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue); + } + + public void saveState() { + saveQueues(); + savePosition(); + savePositionInTrack(); } public int seek(int millis) { @@ -1076,73 +1067,6 @@ public class MusicService extends Service implements } } - 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; - } - } - - public void toggleShuffle() { - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - setShuffleMode(SHUFFLE_MODE_SHUFFLE); - } else { - setShuffleMode(SHUFFLE_MODE_NONE); - } - } - - public int getShuffleMode() { - return shuffleMode; - } - - public void setShuffleMode(final int shuffleMode) { - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putInt(SAVED_SHUFFLE_MODE, shuffleMode) - .apply(); - switch (shuffleMode) { - case SHUFFLE_MODE_SHUFFLE: - this.shuffleMode = shuffleMode; - if (this.getPlayingQueue() != null) { - ShuffleHelper.INSTANCE.makeShuffleList(this.getPlayingQueue(), getPosition()); - } - position = 0; - break; - case SHUFFLE_MODE_NONE: - this.shuffleMode = shuffleMode; - int currentSongId = Objects.requireNonNull(getCurrentSong()).getId(); - playingQueue = new ArrayList<>(originalPlayingQueue); - int newPosition = 0; - if (getPlayingQueue() != null) { - for (Song song : getPlayingQueue()) { - if (song.getId() == currentSongId) { - newPosition = getPlayingQueue().indexOf(song); - } - } - } - position = newPosition; - break; - } - handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); - notifyChange(QUEUE_CHANGED); - } - - public void notifyChange(@NonNull final String what) { - handleAndSendChangeInternal(what); - sendPublicIntent(what); - } - - public void handleAndSendChangeInternal(@NonNull final String what) { - handleChangeInternal(what); - sendChangeInternal(what); - } - // to let other apps know whats playing. i.E. last.fm (scrobbling) or musixmatch public void sendPublicIntent(@NonNull final String what) { final Intent intent = new Intent(what.replace(RETRO_MUSIC_PACKAGE_NAME, MUSIC_PACKAGE_NAME)); @@ -1162,13 +1086,96 @@ public class MusicService extends Service implements } } - private void sendChangeInternal(final String what) { - sendBroadcast(new Intent(what)); - appWidgetBig.notifyChange(this, what); - appWidgetClassic.notifyChange(this, what); - appWidgetSmall.notifyChange(this, what); - appWidgetCard.notifyChange(this, what); - appWidgetText.notifyChange(this, what); + public void toggleShuffle() { + if (getShuffleMode() == SHUFFLE_MODE_NONE) { + setShuffleMode(SHUFFLE_MODE_SHUFFLE); + } else { + setShuffleMode(SHUFFLE_MODE_NONE); + } + } + + public void updateMediaSessionPlaybackState() { + PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder() + .setActions(MEDIA_SESSION_ACTIONS) + .setState(isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, + getSongProgressMillis(), 1); + + setCustomAction(stateBuilder); + + mediaSession.setPlaybackState(stateBuilder.build()); + } + + public void updateNotification() { + if (playingNotification != null && getCurrentSong().getId() != -1) { + playingNotification.update(); + } + } + + void updateMediaSessionMetaData() { + final Song song = getCurrentSong(); + + if (song.getId() == -1) { + 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) + .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getPlayingQueue().size()); + + if (PreferenceUtil.getInstance(this).albumArtOnLockscreen()) { + final Point screenSize = RetroUtil.getScreenSize(MusicService.this); + final BitmapRequestBuilder request = SongGlideRequest.Builder + .from(Glide.with(MusicService.this), song) + .checkIgnoreMediaStore(MusicService.this) + .asBitmap().build(); + if (PreferenceUtil.getInstance(this).blurredAlbumArt()) { + request.transform(new BlurTransformation.Builder(MusicService.this).build()); + } + runOnUiThread(new Runnable() { + @Override + public void run() { + request.into(new SimpleTarget(screenSize.x, screenSize.y) { + @Override + public void onLoadFailed(Exception e, Drawable errorDrawable) { + super.onLoadFailed(e, errorDrawable); + mediaSession.setMetadata(metaData.build()); + } + + @Override + public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { + metaData.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, copy(resource)); + mediaSession.setMetadata(metaData.build()); + } + }); + } + }); + } else { + mediaSession.setMetadata(metaData.build()); + } + } + + private void closeAudioEffectSession() { + final Intent audioEffectsIntent = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + if (playback != null) { + audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playback.getAudioSessionId()); + } + audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(audioEffectsIntent); + } + + private AudioManager getAudioManager() { + if (audioManager == null) { + audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + } + return audioManager; } private void handleChangeInternal(@NonNull final String what) { @@ -1211,55 +1218,55 @@ public class MusicService extends Service implements } } - public int getAudioSessionId() { - if (playback != null) { - return playback.getAudioSessionId(); - } - return -1; - } - - @NonNull - public MediaSessionCompat getMediaSession() { - return mediaSession; - } - - public void releaseWakeLock() { - if (wakeLock.isHeld()) { - wakeLock.release(); - } - } - - public void acquireWakeLock(long milli) { - wakeLock.acquire(milli); - } - - @Override - public void onSharedPreferenceChanged(@NonNull SharedPreferences sharedPreferences, @NonNull String key) { - switch (key) { - case PreferenceUtil.GAPLESS_PLAYBACK: - if (sharedPreferences.getBoolean(key, false)) { - prepareNext(); - } else { - if (playback != null) { - playback.setNextDataSource(null); - } + private boolean openCurrent() { + synchronized (this) { + try { + if (playback != null) { + return playback.setDataSource(getTrackUri(Objects.requireNonNull(getCurrentSong()))); } - break; - case PreferenceUtil.ALBUM_ART_ON_LOCKSCREEN: - case PreferenceUtil.BLURRED_ALBUM_ART: - updateMediaSessionMetaData(); - break; - case PreferenceUtil.COLORED_NOTIFICATION: - case PreferenceUtil.DOMINANT_COLOR: - updateNotification(); - break; - case PreferenceUtil.CLASSIC_NOTIFICATION: - initNotification(); - updateNotification(); - break; - case PreferenceUtil.TOGGLE_HEADSET: - registerHeadsetEvents(); - break; + } catch (Exception e) { + return false; + } + } + return false; + } + + private void playFromPlaylist(Intent intent) { + Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); + int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode()); + if (playlist != null) { + ArrayList playlistSongs = playlist.getSongs(getApplicationContext()); + if (!playlistSongs.isEmpty()) { + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + int startPosition = new Random().nextInt(playlistSongs.size()); + openQueue(playlistSongs, startPosition, true); + setShuffleMode(shuffleMode); + } else { + 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(); + } + } + + private void prepareNext() { + playerHandler.removeMessages(PREPARE_NEXT); + playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget(); + } + + 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); + } } } @@ -1270,29 +1277,119 @@ public class MusicService extends Service implements } } - @Override - public void onTrackWentToNext() { - playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT); + private void releaseResources() { + playerHandler.removeCallbacksAndMessages(null); + musicPlayerHandlerThread.quitSafely(); + queueSaveHandler.removeCallbacksAndMessages(null); + queueSaveHandlerThread.quitSafely(); + if (playback != null) { + playback.release(); + } + playback = null; + mediaSession.release(); } - @Override - public void onTrackEnded() { - acquireWakeLock(30000); - playerHandler.sendEmptyMessage(TRACK_ENDED); + private boolean requestFocus() { + return (getAudioManager() + .requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) + == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); } - public boolean isPausedByTransientLossOfFocus() { - return pausedByTransientLossOfFocus; + private void restoreState() { + shuffleMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_SHUFFLE_MODE, 0); + repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_REPEAT_MODE, 0); + handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); + handleAndSendChangeInternal(REPEAT_MODE_CHANGED); + + playerHandler.removeMessages(RESTORE_QUEUES); + playerHandler.sendEmptyMessage(RESTORE_QUEUES); } - public void setPausedByTransientLossOfFocus(boolean pausedByTransientLossOfFocus) { - this.pausedByTransientLossOfFocus = pausedByTransientLossOfFocus; + private void savePosition() { + PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(SAVED_POSITION, getPosition()).apply(); } - public class MusicBinder extends Binder { - @NonNull - public MusicService getService() { - return MusicService.this; + private void saveQueues() { + queueSaveHandler.removeMessages(SAVE_QUEUES); + queueSaveHandler.sendEmptyMessage(SAVE_QUEUES); + } + + private void sendChangeInternal(final String what) { + sendBroadcast(new Intent(what)); + appWidgetBig.notifyChange(this, what); + appWidgetClassic.notifyChange(this, what); + appWidgetSmall.notifyChange(this, what); + appWidgetCard.notifyChange(this, what); + appWidgetText.notifyChange(this, what); + } + + private void setCustomAction(PlaybackStateCompat.Builder stateBuilder) { + int repeatIcon = R.drawable.ic_repeat_white_24dp; // REPEAT_MODE_NONE + if (getRepeatMode() == REPEAT_MODE_THIS) { + repeatIcon = R.drawable.ic_repeat_one_white_24dp; + } else if (getRepeatMode() == REPEAT_MODE_ALL) { + repeatIcon = R.drawable.ic_repeat_white_circle_24dp; + } + stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( + CYCLE_REPEAT, getString(R.string.action_cycle_repeat), repeatIcon) + .build()); + + final int shuffleIcon = getShuffleMode() == SHUFFLE_MODE_NONE ? R.drawable.ic_shuffle_off_circled + : R.drawable.ic_shuffle_on_circled; + stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( + TOGGLE_SHUFFLE, getString(R.string.action_toggle_shuffle), shuffleIcon) + .build()); + + final int favoriteIcon = MusicUtil.isFavorite(getApplicationContext(), getCurrentSong()) + ? R.drawable.ic_favorite_white_24dp : R.drawable.ic_favorite_border_white_24dp; + stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( + TOGGLE_FAVORITE, getString(R.string.action_toggle_favorite), favoriteIcon) + .build()); + } + + private void setupMediaSession() { + 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, + "RetroMusicPlayer", + mediaButtonReceiverComponentName, + mediaButtonReceiverPendingIntent); + MediaSessionCallback mediasessionCallback = new MediaSessionCallback( + getApplicationContext(), this); + mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS + ); + mediaSession.setCallback(mediasessionCallback); + mediaSession.setActive(true); + mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent); + + } + + private static Bitmap copy(Bitmap bitmap) { + Bitmap.Config config = bitmap.getConfig(); + if (config == null) { + config = Bitmap.Config.RGB_565; + } + try { + return bitmap.copy(config, false); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + return null; } } + + private static String getTrackUri(@NonNull Song song) { + return MusicUtil.getSongFileUri(song.getId()).toString(); + } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java index 243965dc9..92eed06fc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java @@ -14,17 +14,6 @@ package code.name.monkey.retromusic.service; -import android.media.AudioManager; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; - -import androidx.annotation.NonNull; - -import java.lang.ref.WeakReference; - -import code.name.monkey.retromusic.util.PreferenceUtil; - import static code.name.monkey.retromusic.service.MusicService.DUCK; import static code.name.monkey.retromusic.service.MusicService.META_CHANGED; import static code.name.monkey.retromusic.service.MusicService.PLAY_STATE_CHANGED; @@ -32,10 +21,20 @@ import static code.name.monkey.retromusic.service.MusicService.REPEAT_MODE_NONE; import static code.name.monkey.retromusic.service.MusicService.TRACK_ENDED; import static code.name.monkey.retromusic.service.MusicService.TRACK_WENT_TO_NEXT; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.NonNull; +import code.name.monkey.retromusic.util.PreferenceUtil; +import java.lang.ref.WeakReference; + class PlaybackHandler extends Handler { + + private float currentDuckVolume = 1.0f; + @NonNull private final WeakReference mService; - private float currentDuckVolume = 1.0f; PlaybackHandler(final MusicService service, @NonNull final Looper looper) { super(looper); @@ -79,9 +78,14 @@ class PlaybackHandler extends Handler { break; case TRACK_WENT_TO_NEXT: - if (service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { + if (service.pendingQuit || service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { service.pause(); service.seek(0); + if (service.pendingQuit) { + service.pendingQuit = false; + service.quit(); + break; + } } else { service.position = service.nextPosition; service.prepareNextImpl(); diff --git a/app/src/main/java/code/name/monkey/retromusic/util/CustomArtistImageUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/CustomArtistImageUtil.kt index e67f8ba58..46dfe37dc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/CustomArtistImageUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/CustomArtistImageUtil.kt @@ -21,6 +21,7 @@ import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri import android.os.AsyncTask +import android.provider.MediaStore import android.widget.Toast import code.name.monkey.retromusic.App import code.name.monkey.retromusic.model.Artist @@ -32,7 +33,7 @@ import java.io.BufferedOutputStream import java.io.File import java.io.FileOutputStream import java.io.IOException -import java.util.* +import java.util.Locale class CustomArtistImageUtil private constructor(context: Context) { @@ -80,7 +81,7 @@ class CustomArtistImageUtil private constructor(context: Context) { if (succesful) { mPreferences.edit().putBoolean(getFileName(artist), true).commit() ArtistSignatureUtil.getInstance(App.getContext()).updateArtistSignature(artist.name) - App.getContext().contentResolver.notifyChange(Uri.parse("content://media"), null) // trigger media store changed to force artist image reload + App.getContext().contentResolver.notifyChange(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, null) // trigger media store changed to force artist image reload } return null } @@ -95,7 +96,7 @@ class CustomArtistImageUtil private constructor(context: Context) { override fun doInBackground(vararg params: Void): Void? { mPreferences.edit().putBoolean(getFileName(artist), false).commit() ArtistSignatureUtil.getInstance(App.getContext()).updateArtistSignature(artist.name) - App.getContext().contentResolver.notifyChange(Uri.parse("content://media"), null) // trigger media store changed to force artist image reload + App.getContext().contentResolver.notifyChange(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, null) // trigger media store changed to force artist image reload val file = getFile(artist) if (!file.exists()) { diff --git a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java index 63c2d1b45..440bc6d15 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java @@ -103,6 +103,7 @@ public class MusicUtil { ContentResolver contentResolver = context.getContentResolver(); Uri localUri = Uri.parse("content://media/external/audio/albumart"); contentResolver.delete(ContentUris.withAppendedId(localUri, albumId), null, null); + contentResolver.notifyChange(localUri, null); } public static void deleteTracks( @@ -149,8 +150,8 @@ public class MusicUtil { cursor.moveToFirst(); while (!cursor.isAfterLast()) { final int id = cursor.getInt(0); - final Song song = SongLoader.INSTANCE.getSong(activity, id); - MusicPlayerRemote.INSTANCE.removeFromQueue(song); + final Song song = SongLoader.getSong(activity, id); + MusicPlayerRemote.removeFromQueue(song); cursor.moveToNext(); } @@ -325,14 +326,6 @@ public class MusicUtil { return ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, songId); } - @NonNull - public static String getSongInfoString(@NonNull Song song) { - return MusicUtil.buildInfoString( - song.getArtistName(), - song.getAlbumName() - ); - } - public static long getTotalDuration(@NonNull List songs) { long duration = 0; for (int i = 0; i < songs.size(); i++) { @@ -366,6 +359,7 @@ public class MusicUtil { values.put("_data", path); contentResolver.insert(artworkUri, values); + contentResolver.notifyChange(artworkUri, null); } public static boolean isArtistNameUnknown(@Nullable String artistName) { diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java index da5d3b8b1..07c5dce67 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java @@ -14,6 +14,8 @@ package code.name.monkey.retromusic.util; +import static android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; + import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -23,54 +25,72 @@ import android.os.Environment; import android.provider.BaseColumns; import android.provider.MediaStore; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.helper.M3UWriter; import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.PlaylistSong; import code.name.monkey.retromusic.model.Song; - -import static android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; public class PlaylistsUtil { - public static boolean doesPlaylistExist(@NonNull final Context context, final int playlistId) { - return playlistId != -1 && doesPlaylistExist(context, - MediaStore.Audio.Playlists._ID + "=?", - new String[]{String.valueOf(playlistId)}); - } - public static boolean doesPlaylistExist(@NonNull final Context context, final String name) { - return doesPlaylistExist(context, - MediaStore.Audio.PlaylistsColumns.NAME + "=?", - new String[]{name}); + public static void addToPlaylist(@NonNull Context context, + @NonNull List songs, + int playlistId, + boolean showToastOnFinish) { + + ArrayList noSongs = new ArrayList(); + for (Song song : songs) { + if (!doPlaylistContains(context, playlistId, song.getId())) { + noSongs.add(song); + } + } + + final int size = noSongs.size(); + final ContentResolver resolver = context.getContentResolver(); + final String[] projection = new String[]{"max(" + MediaStore.Audio.Playlists.Members.PLAY_ORDER + ")",}; + final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); + + int base = 0; + try (Cursor cursor = resolver.query(uri, projection, null, null, null)) { + + if (cursor != null && cursor.moveToFirst()) { + base = cursor.getInt(0) + 1; + } + } catch (SecurityException ignored) { + } + + int numInserted = 0; + for (int offSet = 0; offSet < size; offSet += 1000) { + numInserted += resolver.bulkInsert(uri, makeInsertItems(noSongs, offSet, 1000, base)); + } + + if (showToastOnFinish) { + Toast.makeText(context, context.getResources().getString( + R.string.inserted_x_songs_into_playlist_x, numInserted, getNameForPlaylist(context, playlistId)), + Toast.LENGTH_SHORT).show(); + } } public static int createPlaylist(@NonNull final Context context, @Nullable final String name) { int id = -1; if (name != null && name.length() > 0) { try { - Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI, - new String[]{MediaStore.Audio.Playlists._ID}, - MediaStore.Audio.PlaylistsColumns.NAME + "=?", new String[]{name}, - null); + Cursor cursor = context.getContentResolver() + .query(EXTERNAL_CONTENT_URI, new String[]{MediaStore.Audio.Playlists._ID}, + MediaStore.Audio.PlaylistsColumns.NAME + "=?", new String[]{name}, null); if (cursor == null || cursor.getCount() < 1) { final ContentValues values = new ContentValues(1); values.put(MediaStore.Audio.PlaylistsColumns.NAME, name); - final Uri uri = context.getContentResolver().insert( - EXTERNAL_CONTENT_URI, - values); + final Uri uri = context.getContentResolver().insert(EXTERNAL_CONTENT_URI, values); if (uri != null) { - // Necessary because somehow the MediaStoreObserver is not notified when adding a playlist - context.getContentResolver().notifyChange(Uri.parse("content://media"), null); - Toast.makeText(context, context.getResources().getString(R.string.created_playlist_x, name), Toast.LENGTH_SHORT).show(); + Toast.makeText(context, context.getResources().getString(R.string.created_playlist_x, name), + Toast.LENGTH_SHORT).show(); id = Integer.parseInt(uri.getLastPathSegment()); } } else { @@ -109,55 +129,63 @@ public class PlaylistsUtil { } } - static void addToPlaylist(@NonNull Context context, - @NonNull Song song, - int playlistId, - boolean showToastOnFinish) { - List helperList = new ArrayList<>(); - helperList.add(song); - addToPlaylist(context, helperList, playlistId, showToastOnFinish); - } - - public static void addToPlaylist(@NonNull Context context, - @NonNull List songs, - int playlistId, - boolean showToastOnFinish) { - - ArrayList noSongs = new ArrayList(); - for (Song song : songs) { - if (!doPlaylistContains(context, playlistId, song.getId())) { - noSongs.add(song); + public static boolean doPlaylistContains(@NonNull final Context context, final long playlistId, + final int songId) { + if (playlistId != -1) { + try { + Cursor c = context.getContentResolver().query( + MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId), + new String[]{MediaStore.Audio.Playlists.Members.AUDIO_ID}, + MediaStore.Audio.Playlists.Members.AUDIO_ID + "=?", new String[]{String.valueOf(songId)}, + null); + int count = 0; + if (c != null) { + count = c.getCount(); + c.close(); + } + return count > 0; + } catch (SecurityException ignored) { } } + return false; + } + public static boolean doesPlaylistExist(@NonNull final Context context, final int playlistId) { + return playlistId != -1 && doesPlaylistExist(context, + MediaStore.Audio.Playlists._ID + "=?", + new String[]{String.valueOf(playlistId)}); + } - final int size = noSongs.size(); - final ContentResolver resolver = context.getContentResolver(); - final String[] projection = new String[]{"max(" + MediaStore.Audio.Playlists.Members.PLAY_ORDER + ")",}; - final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); + public static boolean doesPlaylistExist(@NonNull final Context context, final String name) { + return doesPlaylistExist(context, + MediaStore.Audio.PlaylistsColumns.NAME + "=?", + new String[]{name}); + } - - int base = 0; - try (Cursor cursor = resolver.query(uri, projection, null, null, null)) { - - if (cursor != null && cursor.moveToFirst()) { - base = cursor.getInt(0) + 1; + public static String getNameForPlaylist(@NonNull final Context context, final long id) { + try { + Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI, + new String[]{MediaStore.Audio.PlaylistsColumns.NAME}, + BaseColumns._ID + "=?", + new String[]{String.valueOf(id)}, + null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + return cursor.getString(0); + } + } finally { + cursor.close(); + } } } catch (SecurityException ignored) { } - - int numInserted = 0; - for (int offSet = 0; offSet < size; offSet += 1000) - numInserted += resolver.bulkInsert(uri, makeInsertItems(noSongs, offSet, 1000, base)); - - if (showToastOnFinish) { - Toast.makeText(context, context.getResources().getString( - R.string.inserted_x_songs_into_playlist_x, numInserted, getNameForPlaylist(context, playlistId)), Toast.LENGTH_SHORT).show(); - } + return ""; } @NonNull - public static ContentValues[] makeInsertItems(@NonNull final List songs, final int offset, int len, final int base) { + public static ContentValues[] makeInsertItems(@NonNull final List songs, final int offset, int len, + final int base) { if (offset + len > songs.size()) { len = songs.size() - offset; } @@ -172,6 +200,11 @@ public class PlaylistsUtil { return contentValues; } + public static boolean moveItem(@NonNull final Context context, int playlistId, int from, int to) { + return MediaStore.Audio.Playlists.Members.moveItem(context.getContentResolver(), + playlistId, from, to); + } + public static void removeFromPlaylist(@NonNull final Context context, @NonNull final Song song, int playlistId) { Uri uri = MediaStore.Audio.Playlists.Members.getContentUri( "external", playlistId); @@ -194,7 +227,9 @@ public class PlaylistsUtil { } String selection = MediaStore.Audio.Playlists.Members._ID + " in ("; //noinspection unused - for (String selectionArg : selectionArgs) selection += "?, "; + for (String selectionArg : selectionArgs) { + selection += "?, "; + } selection = selection.substring(0, selection.length() - 2) + ")"; try { @@ -203,29 +238,6 @@ public class PlaylistsUtil { } } - public static boolean doPlaylistContains(@NonNull final Context context, final long playlistId, final int songId) { - if (playlistId != -1) { - try { - Cursor c = context.getContentResolver().query( - MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId), - new String[]{MediaStore.Audio.Playlists.Members.AUDIO_ID}, MediaStore.Audio.Playlists.Members.AUDIO_ID + "=?", new String[]{String.valueOf(songId)}, null); - int count = 0; - if (c != null) { - count = c.getCount(); - c.close(); - } - return count > 0; - } catch (SecurityException ignored) { - } - } - return false; - } - - public static boolean moveItem(@NonNull final Context context, int playlistId, int from, int to) { - return MediaStore.Audio.Playlists.Members.moveItem(context.getContentResolver(), - playlistId, from, to); - } - public static void renamePlaylist(@NonNull final Context context, final long id, final String newName) { ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.Audio.PlaylistsColumns.NAME, newName); @@ -234,39 +246,27 @@ public class PlaylistsUtil { contentValues, MediaStore.Audio.Playlists._ID + "=?", new String[]{String.valueOf(id)}); - context.getContentResolver().notifyChange(Uri.parse("content://media"), null); } catch (SecurityException ignored) { } } - public static String getNameForPlaylist(@NonNull final Context context, final long id) { - try { - Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI, - new String[]{MediaStore.Audio.PlaylistsColumns.NAME}, - BaseColumns._ID + "=?", - new String[]{String.valueOf(id)}, - null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return cursor.getString(0); - } - } finally { - cursor.close(); - } - } - } catch (SecurityException ignored) { - } - return ""; - } - @Nullable public static File savePlaylist(@NonNull Context context, - @NonNull Playlist playlist) throws IOException { + @NonNull Playlist playlist) throws IOException { return M3UWriter.write(context, new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist); } - private static boolean doesPlaylistExist(@NonNull Context context, @NonNull final String selection, @NonNull final String[] values) { + static void addToPlaylist(@NonNull Context context, + @NonNull Song song, + int playlistId, + boolean showToastOnFinish) { + List helperList = new ArrayList<>(); + helperList.add(song); + addToPlaylist(context, helperList, playlistId, showToastOnFinish); + } + + private static boolean doesPlaylistExist(@NonNull Context context, @NonNull final String selection, + @NonNull final String[] values) { Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI, new String[]{}, selection, values, null); diff --git a/app/src/main/res/layout/fragment_fit_playback_controls.xml b/app/src/main/res/layout/fragment_fit_playback_controls.xml index 7c19926dc..50dace10c 100644 --- a/app/src/main/res/layout/fragment_fit_playback_controls.xml +++ b/app/src/main/res/layout/fragment_fit_playback_controls.xml @@ -21,6 +21,7 @@ android:id="@+id/songCurrentProgress" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginEnd="8dp" android:gravity="center_vertical|left|end" android:paddingStart="8dp" android:singleLine="true" diff --git a/build.gradle b/build.gradle index 188603a5a..dac465bce 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' + classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.android.tools.build:bundletool:0.9.0' classpath "gradle.plugin.ru.cleverpumpkin.proguard-dictionaries-generator:plugin:1.0.7"