1644 lines
64 KiB
Java
1644 lines
64 KiB
Java
/*
|
|
* Copyright (c) 2019 Hemanth Savarala.
|
|
*
|
|
* Licensed under the GNU General Public License v3
|
|
*
|
|
* This is free software: you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
* See the GNU General Public License for more details.
|
|
*/
|
|
|
|
package code.name.monkey.retromusic.service;
|
|
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.appwidget.AppWidgetManager;
|
|
import android.bluetooth.BluetoothDevice;
|
|
import android.content.*;
|
|
import android.content.pm.ServiceInfo;
|
|
import android.database.ContentObserver;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Point;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.media.AudioManager;
|
|
import android.media.audiofx.AudioEffect;
|
|
import android.os.*;
|
|
import android.os.Build.VERSION;
|
|
import android.os.Process;
|
|
import android.os.Build.VERSION_CODES;
|
|
import android.provider.MediaStore;
|
|
import android.support.v4.media.MediaBrowserCompat;
|
|
import android.support.v4.media.MediaDescriptionCompat;
|
|
import android.support.v4.media.MediaMetadataCompat;
|
|
import android.support.v4.media.session.MediaSessionCompat;
|
|
import android.support.v4.media.session.PlaybackStateCompat;
|
|
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.media.MediaBrowserServiceCompat;
|
|
import androidx.preference.PreferenceManager;
|
|
import code.name.monkey.appthemehelper.util.VersionUtils;
|
|
import code.name.monkey.retromusic.R;
|
|
import code.name.monkey.retromusic.activities.LockScreenActivity;
|
|
import code.name.monkey.retromusic.appwidgets.*;
|
|
import code.name.monkey.retromusic.auto.AutoMediaIDHelper;
|
|
import code.name.monkey.retromusic.auto.AutoMusicProvider;
|
|
import code.name.monkey.retromusic.glide.BlurTransformation;
|
|
import code.name.monkey.retromusic.glide.GlideApp;
|
|
import code.name.monkey.retromusic.glide.RetroGlideExtension;
|
|
import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
|
import code.name.monkey.retromusic.helper.ShuffleHelper;
|
|
import code.name.monkey.retromusic.model.Song;
|
|
import code.name.monkey.retromusic.model.smartplaylist.AbsSmartPlaylist;
|
|
import code.name.monkey.retromusic.providers.HistoryStore;
|
|
import code.name.monkey.retromusic.providers.MusicPlaybackQueueStore;
|
|
import code.name.monkey.retromusic.providers.SongPlayCountStore;
|
|
import code.name.monkey.retromusic.service.notification.PlayingNotification;
|
|
import code.name.monkey.retromusic.service.notification.PlayingNotificationImpl;
|
|
import code.name.monkey.retromusic.service.notification.PlayingNotificationOreo;
|
|
import code.name.monkey.retromusic.service.playback.Playback;
|
|
import code.name.monkey.retromusic.util.MusicUtil;
|
|
import code.name.monkey.retromusic.util.PackageValidator;
|
|
import code.name.monkey.retromusic.util.PreferenceUtil;
|
|
import code.name.monkey.retromusic.util.RetroUtil;
|
|
import code.name.monkey.retromusic.volume.AudioVolumeObserver;
|
|
import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener;
|
|
import com.bumptech.glide.RequestBuilder;
|
|
import com.bumptech.glide.request.target.SimpleTarget;
|
|
import kotlin.Unit;
|
|
import software.lavender.music.player.ExoPlayerPlayback;
|
|
|
|
import java.util.*;
|
|
|
|
import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE;
|
|
import static androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT;
|
|
import static code.name.monkey.retromusic.ConstantsKt.*;
|
|
import static code.name.monkey.retromusic.service.AudioFader.startFadeAnimator;
|
|
import static org.koin.java.KoinJavaComponent.get;
|
|
|
|
/**
|
|
* @author Karim Abou Zeid (kabouzeid), Andrew Neal
|
|
*/
|
|
public class MusicService extends MediaBrowserServiceCompat
|
|
implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks, OnAudioVolumeChangedListener {
|
|
|
|
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
|
|
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
|
| 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;
|
|
|
|
private PackageValidator mPackageValidator;
|
|
|
|
private final AutoMusicProvider mMusicProvider = get(AutoMusicProvider.class);
|
|
|
|
public boolean trackEndedByCrossfade = false;
|
|
|
|
public int position = -1;
|
|
|
|
private final AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance();
|
|
|
|
private final AppWidgetCard appWidgetCard = AppWidgetCard.Companion.getInstance();
|
|
|
|
private final AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance();
|
|
|
|
private final AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance();
|
|
|
|
private final AppWidgetText appWidgetText = AppWidgetText.Companion.getInstance();
|
|
|
|
private final AppWidgetMD3 appWidgetMd3 = AppWidgetMD3.Companion.getInstance();
|
|
|
|
private final BroadcastReceiver widgetIntentReceiver =
|
|
new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(final Context context, final Intent intent) {
|
|
final String command = intent.getStringExtra(EXTRA_APP_WIDGET_NAME);
|
|
final int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
|
|
if (command != null) {
|
|
switch (command) {
|
|
case AppWidgetClassic.NAME: {
|
|
appWidgetClassic.performUpdate(MusicService.this, ids);
|
|
break;
|
|
}
|
|
case AppWidgetSmall.NAME: {
|
|
appWidgetSmall.performUpdate(MusicService.this, ids);
|
|
break;
|
|
}
|
|
case AppWidgetBig.NAME: {
|
|
appWidgetBig.performUpdate(MusicService.this, ids);
|
|
break;
|
|
}
|
|
case AppWidgetCard.NAME: {
|
|
appWidgetCard.performUpdate(MusicService.this, ids);
|
|
break;
|
|
}
|
|
case AppWidgetText.NAME: {
|
|
appWidgetText.performUpdate(MusicService.this, ids);
|
|
break;
|
|
}
|
|
case AppWidgetMD3.NAME: {
|
|
appWidgetMd3.performUpdate(MusicService.this, ids);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
private AudioManager audioManager;
|
|
private final IntentFilter becomingNoisyReceiverIntentFilter =
|
|
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
|
private boolean becomingNoisyReceiverRegistered;
|
|
private final IntentFilter bluetoothConnectedIntentFilter =
|
|
new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED);
|
|
private boolean bluetoothConnectedRegistered = false;
|
|
private final IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
|
|
private boolean headsetReceiverRegistered = false;
|
|
private MediaSessionCompat mediaSession;
|
|
private ContentObserver mediaStoreObserver;
|
|
private HandlerThread musicPlayerHandlerThread;
|
|
private boolean notHandledMetaChangedForCurrentTrack;
|
|
private List<Song> originalPlayingQueue = new ArrayList<>();
|
|
private List<Song> playingQueue = 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 final BroadcastReceiver updateFavoriteReceiver =
|
|
new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(final Context context, final Intent intent) {
|
|
playingNotification.updateFavorite(getCurrentSong(), MusicService.this::startForegroundOrNotify);
|
|
startForegroundOrNotify();
|
|
}
|
|
};
|
|
private final BroadcastReceiver lockScreenReceiver =
|
|
new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (PreferenceUtil.INSTANCE.isLockScreen() && isPlaying()) {
|
|
Intent lockIntent = new Intent(context, LockScreenActivity.class);
|
|
lockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
startActivity(lockIntent);
|
|
}
|
|
}
|
|
};
|
|
private QueueSaveHandler queueSaveHandler;
|
|
private HandlerThread queueSaveHandlerThread;
|
|
private boolean queuesRestored;
|
|
private int repeatMode;
|
|
private int shuffleMode;
|
|
private final SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper();
|
|
private final BroadcastReceiver bluetoothReceiver =
|
|
new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(final Context context, final Intent intent) {
|
|
String action = intent.getAction();
|
|
if (action != null) {
|
|
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)
|
|
&& PreferenceUtil.INSTANCE.isBluetoothSpeaker()) {
|
|
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
|
if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) {
|
|
play();
|
|
}
|
|
} else {
|
|
if (getAudioManager().isBluetoothA2dpOn()) {
|
|
play();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
private final 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 final 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:
|
|
pause();
|
|
break;
|
|
case 1:
|
|
play();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
private ThrottledSeekHandler throttledSeekHandler;
|
|
private Handler uiThreadHandler;
|
|
private PowerManager.WakeLock wakeLock;
|
|
private NotificationManager notificationManager;
|
|
private boolean isForeground = false;
|
|
|
|
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 song.getData();//MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString();
|
|
}
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
final TelephonyManager telephonyManager =
|
|
(TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
|
if (telephonyManager != null) {
|
|
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
|
}
|
|
|
|
final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
|
if (powerManager != null) {
|
|
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
|
|
}
|
|
wakeLock.setReferenceCounted(false);
|
|
|
|
musicPlayerHandlerThread = new HandlerThread("PlaybackHandler");
|
|
musicPlayerHandlerThread.start();
|
|
playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper());
|
|
|
|
// Set MultiPlayer when crossfade duration is 0 i.e. off
|
|
// TODO: do crossfading in exoplayer or remove the feature entirely for now, crossfadeplayer will NOT work right now
|
|
if (PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) {
|
|
playback = new ExoPlayerPlayback(this);
|
|
} else {
|
|
playback = new CrossFadePlayer(this);
|
|
}
|
|
|
|
playback.setCallbacks(this);
|
|
|
|
setupMediaSession();
|
|
|
|
// queue saving needs to run on a separate thread so that it doesn't block the playback handler
|
|
// events
|
|
queueSaveHandlerThread =
|
|
new HandlerThread("QueueSaveHandler", Process.THREAD_PRIORITY_BACKGROUND);
|
|
queueSaveHandlerThread.start();
|
|
queueSaveHandler = new QueueSaveHandler(this, queueSaveHandlerThread.getLooper());
|
|
|
|
uiThreadHandler = new Handler();
|
|
|
|
registerReceiver(widgetIntentReceiver, new IntentFilter(APP_WIDGET_UPDATE));
|
|
registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED));
|
|
registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
|
|
|
|
setSessionToken(mediaSession.getSessionToken());
|
|
if (VersionUtils.INSTANCE.hasMarshmallow()) {
|
|
notificationManager = getSystemService(NotificationManager.class);
|
|
}
|
|
initNotification();
|
|
|
|
mediaStoreObserver = new MediaStoreObserver(this, playerHandler);
|
|
throttledSeekHandler = new ThrottledSeekHandler(this, playerHandler);
|
|
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);
|
|
|
|
AudioVolumeObserver audioVolumeObserver = new AudioVolumeObserver(this);
|
|
audioVolumeObserver.register(AudioManager.STREAM_MUSIC, this);
|
|
|
|
PreferenceUtil.INSTANCE.registerOnSharedPreferenceChangedListener(this);
|
|
|
|
restoreState();
|
|
|
|
sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_CREATED"));
|
|
|
|
registerHeadsetEvents();
|
|
registerBluetoothConnected();
|
|
|
|
mPackageValidator = new PackageValidator(this, R.xml.allowed_media_browser_callers);
|
|
mMusicProvider.setMusicService(this);
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
unregisterReceiver(widgetIntentReceiver);
|
|
unregisterReceiver(updateFavoriteReceiver);
|
|
unregisterReceiver(lockScreenReceiver);
|
|
if (becomingNoisyReceiverRegistered) {
|
|
unregisterReceiver(becomingNoisyReceiver);
|
|
becomingNoisyReceiverRegistered = false;
|
|
}
|
|
if (headsetReceiverRegistered) {
|
|
unregisterReceiver(headsetReceiver);
|
|
headsetReceiverRegistered = false;
|
|
}
|
|
if (bluetoothConnectedRegistered) {
|
|
unregisterReceiver(bluetoothReceiver);
|
|
bluetoothConnectedRegistered = false;
|
|
}
|
|
mediaSession.setActive(false);
|
|
quit();
|
|
releaseResources();
|
|
getContentResolver().unregisterContentObserver(mediaStoreObserver);
|
|
PreferenceUtil.INSTANCE.unregisterOnSharedPreferenceChangedListener(this);
|
|
wakeLock.release();
|
|
|
|
sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_DESTROYED"));
|
|
}
|
|
|
|
public void acquireWakeLock(long milli) {
|
|
wakeLock.acquire(milli);
|
|
}
|
|
|
|
boolean pausedByZeroVolume;
|
|
|
|
@Override
|
|
public void onAudioVolumeChanged(int currentVolume, int maxVolume) {
|
|
if (PreferenceUtil.INSTANCE.isPauseOnZeroVolume()) {
|
|
if (isPlaying() && currentVolume < 1) {
|
|
pause();
|
|
System.out.println("Paused");
|
|
pausedByZeroVolume = true;
|
|
} else if (pausedByZeroVolume && currentVolume >= 1) {
|
|
System.out.println("Played");
|
|
play();
|
|
pausedByZeroVolume = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<Song> songs) {
|
|
playingQueue.addAll(position, songs);
|
|
originalPlayingQueue.addAll(position, songs);
|
|
notifyChange(QUEUE_CHANGED);
|
|
}
|
|
|
|
public void addSongs(List<Song> 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());
|
|
}
|
|
|
|
public Song getNextSong() {
|
|
if (isLastTrack() && getRepeatMode() == REPEAT_MODE_NONE) {
|
|
return null;
|
|
} else {
|
|
return getSongAt(getNextPosition(false));
|
|
}
|
|
}
|
|
|
|
@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 List<Song> 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;
|
|
long 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.INSTANCE.isClassicNotification()) {
|
|
playingNotification = PlayingNotificationImpl.Companion.from(this, notificationManager, mediaSession);
|
|
} else {
|
|
playingNotification = PlayingNotificationOreo.Companion.from(this, notificationManager);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// For Android auto, need to call super, or onGetRoot won't be called.
|
|
if (intent != null && "android.media.browse.MediaBrowserService".equals(intent.getAction())) {
|
|
return super.onBind(intent);
|
|
}
|
|
return musicBind;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
|
|
|
|
|
|
// Check origin to ensure we're not allowing any arbitrary app to browse app contents
|
|
if (!mPackageValidator.isKnownCaller(clientPackageName, clientUid)) {
|
|
// Request from an untrusted package: return an empty browser root
|
|
return new BrowserRoot(AutoMediaIDHelper.MEDIA_ID_EMPTY_ROOT, null);
|
|
} else {
|
|
/**
|
|
* By default return the browsable root. Treat the EXTRA_RECENT flag as a special case
|
|
* and return the recent root instead.
|
|
*/
|
|
boolean isRecentRequest = false;
|
|
if (rootHints != null) {
|
|
isRecentRequest = rootHints.getBoolean(EXTRA_RECENT);
|
|
}
|
|
String browserRootPath;
|
|
if (isRecentRequest) {
|
|
browserRootPath = AutoMediaIDHelper.RECENT_ROOT;
|
|
} else {
|
|
browserRootPath = AutoMediaIDHelper.MEDIA_ID_ROOT;
|
|
}
|
|
return new BrowserRoot(browserRootPath, null);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onLoadChildren(@NonNull String parentId, @NonNull MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>> result) {
|
|
if (parentId.equals(AutoMediaIDHelper.RECENT_ROOT)) {
|
|
Song song = getCurrentSong();
|
|
MediaBrowserCompat.MediaItem mediaItem = new MediaBrowserCompat.MediaItem(
|
|
new MediaDescriptionCompat.Builder()
|
|
.setMediaId(String.valueOf(song.getId()))
|
|
.setTitle(song.getTitle())
|
|
.setSubtitle(song.getArtistName())
|
|
.setIconUri(song.getCoverArtUri())
|
|
.build(), FLAG_PLAYABLE
|
|
);
|
|
result.sendResult(Collections.singletonList(mediaItem));
|
|
} else {
|
|
result.sendResult(mMusicProvider.getChildren(parentId, getResources()));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSharedPreferenceChanged(
|
|
@NonNull SharedPreferences sharedPreferences, @NonNull String key) {
|
|
switch (key) {
|
|
case CROSS_FADE_DURATION:
|
|
int progress = getSongProgressMillis();
|
|
boolean wasPlaying = isPlaying();
|
|
/* Switch to MultiPlayer if Crossfade duration is 0 and
|
|
Playback is not an instance of MultiPlayer */
|
|
if (playback != null)
|
|
playback.setCrossFadeDuration(PreferenceUtil.INSTANCE.getCrossFadeDuration());
|
|
if (!(playback instanceof MultiPlayer) && PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) {
|
|
if (playback != null) {
|
|
playback.release();
|
|
}
|
|
playback = null;
|
|
playback = new ExoPlayerPlayback(this);
|
|
playback.setCallbacks(this);
|
|
if (openTrackAndPrepareNextAt(position)) {
|
|
seek(progress);
|
|
if (wasPlaying) {
|
|
play();
|
|
}
|
|
}
|
|
}
|
|
/* Switch to CrossFadePlayer if Crossfade duration is greater than 0 and
|
|
Playback is not an instance of CrossFadePlayer */
|
|
else if (!(playback instanceof CrossFadePlayer) && PreferenceUtil.INSTANCE.getCrossFadeDuration() > 0) {
|
|
if (playback != null) {
|
|
playback.release();
|
|
}
|
|
playback = null;
|
|
playback = new CrossFadePlayer(this);
|
|
playback.setCallbacks(this);
|
|
if (openTrackAndPrepareNextAt(position)) {
|
|
seek(progress);
|
|
if (wasPlaying) {
|
|
play();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case ALBUM_ART_ON_LOCK_SCREEN:
|
|
case BLURRED_ALBUM_ART:
|
|
updateMediaSessionMetaData();
|
|
break;
|
|
case COLORED_NOTIFICATION:
|
|
updateNotification();
|
|
break;
|
|
case CLASSIC_NOTIFICATION:
|
|
initNotification();
|
|
updateNotification();
|
|
break;
|
|
case TOGGLE_HEADSET:
|
|
registerHeadsetEvents();
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
|
|
if (intent != null && intent.getAction() != null) {
|
|
restoreQueuesAndPositionIfNecessary();
|
|
String action = intent.getAction();
|
|
switch (action) {
|
|
case ACTION_TOGGLE_PAUSE:
|
|
if (isPlaying()) {
|
|
pause();
|
|
} else {
|
|
play();
|
|
}
|
|
break;
|
|
case ACTION_PAUSE:
|
|
pause();
|
|
break;
|
|
case ACTION_PLAY:
|
|
play();
|
|
break;
|
|
case ACTION_PLAY_PLAYLIST:
|
|
playFromPlaylist(intent);
|
|
break;
|
|
case ACTION_REWIND:
|
|
back(true);
|
|
break;
|
|
case ACTION_SKIP:
|
|
playNextSong(true);
|
|
break;
|
|
case ACTION_STOP:
|
|
case ACTION_QUIT:
|
|
pendingQuit = false;
|
|
quit();
|
|
break;
|
|
case ACTION_PENDING_QUIT:
|
|
pendingQuit = true;
|
|
break;
|
|
case TOGGLE_FAVORITE:
|
|
MusicUtil.INSTANCE.toggleFavorite(getApplicationContext(), getCurrentSong());
|
|
break;
|
|
}
|
|
}
|
|
|
|
return START_NOT_STICKY;
|
|
}
|
|
|
|
@Override
|
|
public void onTrackEnded() {
|
|
acquireWakeLock(30000);
|
|
playerHandler.sendEmptyMessage(TRACK_ENDED);
|
|
}
|
|
|
|
@Override
|
|
public void onTrackEndedWithCrossfade() {
|
|
trackEndedByCrossfade = true;
|
|
acquireWakeLock(30000);
|
|
playerHandler.sendEmptyMessage(TRACK_ENDED);
|
|
}
|
|
|
|
@Override
|
|
public void onTrackWentToNext() {
|
|
playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
|
|
}
|
|
|
|
@Override
|
|
public boolean onUnbind(Intent intent) {
|
|
if (!isPlaying()) {
|
|
stopSelf();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void openQueue(
|
|
@Nullable final List<Song> 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);
|
|
|
|
int position = startPosition;
|
|
if (shuffleMode == SHUFFLE_MODE_SHUFFLE) {
|
|
ShuffleHelper.INSTANCE.makeShuffleList(this.playingQueue, startPosition);
|
|
position = 0;
|
|
}
|
|
if (startPlaying) {
|
|
playSongAt(position);
|
|
} else {
|
|
setPosition(position);
|
|
}
|
|
notifyChange(QUEUE_CHANGED);
|
|
}
|
|
}
|
|
|
|
public boolean openTrackAndPrepareNextAt(int position) {
|
|
synchronized (this) {
|
|
this.position = position;
|
|
boolean prepared = openCurrent();
|
|
if (prepared) {
|
|
prepareNextImpl();
|
|
}
|
|
notifyChange(META_CHANGED);
|
|
notHandledMetaChangedForCurrentTrack = false;
|
|
return prepared;
|
|
}
|
|
}
|
|
|
|
public void pause() {
|
|
Log.i(TAG, "Paused");
|
|
pausedByTransientLossOfFocus = false;
|
|
if (playback != null && playback.isPlaying()) {
|
|
startFadeAnimator(playback, false, () -> {
|
|
//Code to run when Animator Ends
|
|
if (playback != null) {
|
|
playback.pause();
|
|
}
|
|
notifyChange(PLAY_STATE_CHANGED);
|
|
});
|
|
}
|
|
}
|
|
|
|
public void forcePause() {
|
|
pausedByTransientLossOfFocus = false;
|
|
if (playback != null && playback.isPlaying()) {
|
|
playback.pause();
|
|
notifyChange(PLAY_STATE_CHANGED);
|
|
}
|
|
}
|
|
|
|
public void play() {
|
|
synchronized (this) {
|
|
if (requestFocus()) {
|
|
if (playback != null && !playback.isPlaying()) {
|
|
if (!playback.isInitialized()) {
|
|
playSongAt(getPosition());
|
|
} else {
|
|
//Don't Start playing when it's casting
|
|
if (MusicPlayerRemote.INSTANCE.isCasting()) {
|
|
return;
|
|
}
|
|
startFadeAnimator(playback, true, () -> {
|
|
// Code when Animator Ends
|
|
if (!becomingNoisyReceiverRegistered) {
|
|
registerReceiver(becomingNoisyReceiver, becomingNoisyReceiverIntentFilter);
|
|
becomingNoisyReceiverRegistered = true;
|
|
}
|
|
if (notHandledMetaChangedForCurrentTrack) {
|
|
handleChangeInternal(META_CHANGED);
|
|
notHandledMetaChangedForCurrentTrack = false;
|
|
}
|
|
|
|
// fixes a bug where the volume would stay ducked because the
|
|
// AudioManager.AUDIOFOCUS_GAIN event is not sent
|
|
playerHandler.removeMessages(DUCK);
|
|
playerHandler.sendEmptyMessage(UNDUCK);
|
|
});
|
|
//Start Playback with Animator
|
|
playback.start();
|
|
notifyChange(PLAY_STATE_CHANGED);
|
|
}
|
|
}
|
|
} else {
|
|
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 (!trackEndedByCrossfade) {
|
|
// This is only imp if we are using crossfade
|
|
if (playback instanceof CrossFadePlayer) {
|
|
((CrossFadePlayer) playback).sourceChangedByUser();
|
|
}
|
|
} else {
|
|
trackEndedByCrossfade = false;
|
|
}
|
|
if (openTrackAndPrepareNextAt(position)) {
|
|
play();
|
|
} else {
|
|
Toast.makeText(this, getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT)
|
|
.show();
|
|
}
|
|
}
|
|
|
|
public void playSongs(ArrayList<Song> songs, int shuffleMode) {
|
|
if (songs != null && !songs.isEmpty()) {
|
|
if (shuffleMode == SHUFFLE_MODE_SHUFFLE) {
|
|
int startPosition = new Random().nextInt(songs.size());
|
|
openQueue(songs, startPosition, false);
|
|
setShuffleMode(shuffleMode);
|
|
} else {
|
|
openQueue(songs, 0, false);
|
|
}
|
|
play();
|
|
} else {
|
|
Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show();
|
|
}
|
|
}
|
|
|
|
public void prepareNextImpl() {
|
|
synchronized (this) {
|
|
try {
|
|
int nextPosition = getNextPosition(false);
|
|
if (playback != null) {
|
|
playback.setNextDataSource(getTrackUri(Objects.requireNonNull(getSongAt(nextPosition))));
|
|
}
|
|
this.nextPosition = nextPosition;
|
|
} catch (Exception ignored) {
|
|
}
|
|
}
|
|
}
|
|
|
|
public void quit() {
|
|
pause();
|
|
stopForeground(true);
|
|
notificationManager.cancel(PlayingNotification.NOTIFICATION_ID);
|
|
|
|
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 {
|
|
originalPlayingQueue.remove(playingQueue.remove(position));
|
|
}
|
|
|
|
rePosition(position);
|
|
|
|
notifyChange(QUEUE_CHANGED);
|
|
}
|
|
|
|
public void removeSongImpl(@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);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void removeSong(@NonNull Song song) {
|
|
removeSongImpl(song);
|
|
notifyChange(QUEUE_CHANGED);
|
|
}
|
|
|
|
public void removeSongs(@NonNull List<Song> songs) {
|
|
for (Song song : songs) {
|
|
removeSongImpl(song);
|
|
}
|
|
notifyChange(QUEUE_CHANGED);
|
|
}
|
|
|
|
public synchronized void restoreQueuesAndPositionIfNecessary() {
|
|
if (!queuesRestored && playingQueue.isEmpty()) {
|
|
List<Song> restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue();
|
|
List<Song> 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 runOnUiThread(Runnable runnable) {
|
|
uiThreadHandler.post(runnable);
|
|
}
|
|
|
|
public void savePositionInTrack() {
|
|
PreferenceManager.getDefaultSharedPreferences(this)
|
|
.edit()
|
|
.putInt(SAVED_POSITION_IN_TRACK, getSongProgressMillis())
|
|
.apply();
|
|
}
|
|
|
|
public void saveQueuesImpl() {
|
|
MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue);
|
|
}
|
|
|
|
public void saveState() {
|
|
saveQueues();
|
|
savePosition();
|
|
savePositionInTrack();
|
|
}
|
|
|
|
public int seek(int millis) {
|
|
synchronized (this) {
|
|
try {
|
|
int newPosition = 0;
|
|
if (playback != null) {
|
|
newPosition = playback.seek(millis);
|
|
}
|
|
throttledSeekHandler.notifySeek();
|
|
return newPosition;
|
|
} catch (Exception e) {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
|
|
final Song song = getCurrentSong();
|
|
|
|
if (song != null) {
|
|
intent.putExtra("id", song.getId());
|
|
intent.putExtra("artist", song.getArtistName());
|
|
intent.putExtra("album", song.getAlbumName());
|
|
intent.putExtra("track", song.getTitle());
|
|
intent.putExtra("duration", song.getDuration());
|
|
intent.putExtra("position", (long) getSongProgressMillis());
|
|
intent.putExtra("playing", isPlaying());
|
|
intent.putExtra("scrobbling_source", RETRO_MUSIC_PACKAGE_NAME);
|
|
sendStickyBroadcast(intent);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
stopForegroundAndNotification();
|
|
initNotification();
|
|
}
|
|
}
|
|
|
|
public void updateMediaSessionMetaData() {
|
|
Log.i(TAG, "onResourceReady: ");
|
|
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);
|
|
|
|
metaData.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getPlayingQueue().size());
|
|
|
|
if (PreferenceUtil.INSTANCE.isAlbumArtOnLockScreen()) {
|
|
final Point screenSize = RetroUtil.getScreenSize(MusicService.this);
|
|
final RequestBuilder<Bitmap> request = GlideApp.with(MusicService.this).asBitmap().songCoverOptions(song).load(RetroGlideExtension.INSTANCE.getSongModel(song));
|
|
if (PreferenceUtil.INSTANCE.isBlurredAlbumArt()) {
|
|
request.transform(new BlurTransformation.Builder(MusicService.this).build());
|
|
}
|
|
runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
request.into(new SimpleTarget<Bitmap>(screenSize.x, screenSize.y) {
|
|
@Override
|
|
public void onLoadFailed(Drawable errorDrawable) {
|
|
super.onLoadFailed(errorDrawable);
|
|
mediaSession.setMetadata(metaData.build());
|
|
}
|
|
|
|
@Override
|
|
public void onResourceReady(@NonNull Bitmap resource, @Nullable com.bumptech.glide.request.transition.Transition<? super Bitmap> transition) {
|
|
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) {
|
|
switch (what) {
|
|
case PLAY_STATE_CHANGED:
|
|
updateMediaSessionPlaybackState();
|
|
final boolean isPlaying = isPlaying();
|
|
if (!isPlaying && getSongProgressMillis() > 0) {
|
|
savePositionInTrack();
|
|
}
|
|
songPlayCountHelper.notifyPlayStateChanged(isPlaying);
|
|
playingNotification.setPlaying(isPlaying);
|
|
startForegroundOrNotify();
|
|
break;
|
|
case FAVORITE_STATE_CHANGED:
|
|
playingNotification.updateFavorite(getCurrentSong(), this::startForegroundOrNotify);
|
|
case META_CHANGED:
|
|
playingNotification.updateMetadata(getCurrentSong(), this::startForegroundOrNotify);
|
|
updateMediaSessionMetaData();
|
|
updateMediaSessionPlaybackState();
|
|
savePosition();
|
|
savePositionInTrack();
|
|
final Song currentSong = getCurrentSong();
|
|
HistoryStore.getInstance(this).addSongId(currentSong.getId());
|
|
if (songPlayCountHelper.shouldBumpPlayCount()) {
|
|
SongPlayCountStore.getInstance(this).bumpPlayCount(songPlayCountHelper.getSong().getId());
|
|
}
|
|
songPlayCountHelper.notifySongChanged(currentSong);
|
|
break;
|
|
case QUEUE_CHANGED:
|
|
updateMediaSessionMetaData(); // because playing queue size might have changed
|
|
saveState();
|
|
if (playingQueue.size() > 0) {
|
|
prepareNext();
|
|
} else {
|
|
stopForegroundAndNotification();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private Unit startForegroundOrNotify() {
|
|
if (!isForeground) {
|
|
// Specify that this is a media service, if supported.
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
startForeground(
|
|
PlayingNotification.NOTIFICATION_ID, playingNotification.build(),
|
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
|
);
|
|
} else {
|
|
startForeground(PlayingNotification.NOTIFICATION_ID, playingNotification.build());
|
|
}
|
|
|
|
isForeground = true;
|
|
} else {
|
|
// If we are already in foreground just update the notification
|
|
notificationManager.notify(
|
|
PlayingNotification.NOTIFICATION_ID, playingNotification.build()
|
|
);
|
|
}
|
|
return Unit.INSTANCE;
|
|
}
|
|
|
|
private void stopForegroundAndNotification() {
|
|
stopForeground(true);
|
|
notificationManager.cancel(PlayingNotification.NOTIFICATION_ID);
|
|
|
|
isForeground = false;
|
|
}
|
|
|
|
private boolean openCurrent() {
|
|
synchronized (this) {
|
|
try {
|
|
if (playback != null) {
|
|
return playback.setDataSource(getTrackUri(Objects.requireNonNull(getCurrentSong())));
|
|
}
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void playFromPlaylist(Intent intent) {
|
|
AbsSmartPlaylist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST);
|
|
int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode());
|
|
if (playlist != null) {
|
|
List<Song> playlistSongs = playlist.songs();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void registerBluetoothConnected() {
|
|
Log.i(TAG, "registerBluetoothConnected: ");
|
|
if (!bluetoothConnectedRegistered) {
|
|
registerReceiver(bluetoothReceiver, bluetoothConnectedIntentFilter);
|
|
bluetoothConnectedRegistered = true;
|
|
}
|
|
}
|
|
|
|
private void registerHeadsetEvents() {
|
|
if (!headsetReceiverRegistered && PreferenceUtil.INSTANCE.isHeadsetPlugged()) {
|
|
registerReceiver(headsetReceiver, headsetReceiverIntentFilter);
|
|
headsetReceiverRegistered = true;
|
|
}
|
|
}
|
|
|
|
private void releaseResources() {
|
|
playerHandler.removeCallbacksAndMessages(null);
|
|
musicPlayerHandlerThread.quitSafely();
|
|
queueSaveHandler.removeCallbacksAndMessages(null);
|
|
queueSaveHandlerThread.quitSafely();
|
|
if (playback != null) {
|
|
playback.release();
|
|
}
|
|
playback = null;
|
|
mediaSession.release();
|
|
}
|
|
|
|
private boolean requestFocus() {
|
|
return (getAudioManager()
|
|
.requestAudioFocus(
|
|
audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
|
|
== AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private void savePosition() {
|
|
PreferenceManager.getDefaultSharedPreferences(this)
|
|
.edit()
|
|
.putInt(SAVED_POSITION, getPosition())
|
|
.apply();
|
|
}
|
|
|
|
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);
|
|
appWidgetMd3.notifyChange(this, what);
|
|
}
|
|
|
|
private void setCustomAction(PlaybackStateCompat.Builder stateBuilder) {
|
|
int repeatIcon = R.drawable.ic_repeat; // REPEAT_MODE_NONE
|
|
if (getRepeatMode() == REPEAT_MODE_THIS) {
|
|
repeatIcon = R.drawable.ic_repeat_one;
|
|
} else if (getRepeatMode() == REPEAT_MODE_ALL) {
|
|
repeatIcon = R.drawable.ic_repeat_white_circle;
|
|
}
|
|
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.INSTANCE.isFavorite(getApplicationContext(), getCurrentSong())
|
|
? R.drawable.ic_favorite
|
|
: R.drawable.ic_favorite_border;
|
|
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;
|
|
mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent,
|
|
VersionUtils.INSTANCE.hasMarshmallow() ? PendingIntent.FLAG_IMMUTABLE : 0);
|
|
|
|
// TODO: change tag
|
|
mediaSession = new MediaSessionCompat(
|
|
this,
|
|
"RetroMusicPlayer",
|
|
mediaButtonReceiverComponentName,
|
|
mediaButtonReceiverPendingIntent);
|
|
MediaSessionCallback mediasessionCallback =
|
|
new MediaSessionCallback(getApplicationContext(), this);
|
|
mediaSession.setCallback(mediasessionCallback);
|
|
mediaSession.setActive(true);
|
|
mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent);
|
|
}
|
|
|
|
public class MusicBinder extends Binder {
|
|
|
|
@NonNull
|
|
public MusicService getService() {
|
|
return MusicService.this;
|
|
}
|
|
}
|
|
} |