1367 lines
52 KiB
Java
1367 lines
52 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.PendingIntent;
|
|
import android.app.Service;
|
|
import android.appwidget.AppWidgetManager;
|
|
import android.bluetooth.BluetoothDevice;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.SharedPreferences;
|
|
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.Binder;
|
|
import android.os.Build;
|
|
import android.os.Build.VERSION;
|
|
import android.os.Build.VERSION_CODES;
|
|
import android.os.Handler;
|
|
import android.os.HandlerThread;
|
|
import android.os.IBinder;
|
|
import android.os.PowerManager;
|
|
import android.os.Process;
|
|
import android.provider.MediaStore;
|
|
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.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;
|
|
import code.name.monkey.retromusic.appwidgets.AppWidgetClassic;
|
|
import code.name.monkey.retromusic.appwidgets.AppWidgetSmall;
|
|
import code.name.monkey.retromusic.appwidgets.AppWidgetText;
|
|
import code.name.monkey.retromusic.glide.BlurTransformation;
|
|
import code.name.monkey.retromusic.glide.SongGlideRequest;
|
|
import code.name.monkey.retromusic.helper.ShuffleHelper;
|
|
import code.name.monkey.retromusic.model.Playlist;
|
|
import code.name.monkey.retromusic.model.Song;
|
|
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.PreferenceUtil;
|
|
import code.name.monkey.retromusic.util.RetroUtil;
|
|
|
|
/**
|
|
* @author Karim Abou Zeid (kabouzeid), Andrew Neal
|
|
*/
|
|
public class MusicService extends Service implements
|
|
SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks {
|
|
|
|
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;
|
|
|
|
public int position = -1;
|
|
|
|
private AppWidgetBig appWidgetBig = AppWidgetBig.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 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
};
|
|
private AudioManager audioManager;
|
|
private IntentFilter becomingNoisyReceiverIntentFilter = new IntentFilter(
|
|
AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
|
private boolean becomingNoisyReceiverRegistered;
|
|
private IntentFilter bluetoothConnectedIntentFilter = new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED);
|
|
private boolean bluetoothConnectedRegistered = false;
|
|
private 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) {
|
|
updateNotification();
|
|
}
|
|
};
|
|
|
|
private QueueSaveHandler queueSaveHandler;
|
|
private HandlerThread queueSaveHandlerThread;
|
|
private boolean queuesRestored;
|
|
private int repeatMode;
|
|
private int shuffleMode;
|
|
private 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.getInstance(context).bluetoothSpeaker()) {
|
|
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
|
if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) {
|
|
play();
|
|
}
|
|
} else {
|
|
if (getAudioManager().isBluetoothA2dpOn()) {
|
|
play();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
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:
|
|
pause();
|
|
break;
|
|
case 1:
|
|
play();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
private ThrottledSeekHandler throttledSeekHandler;
|
|
private Handler uiThreadHandler;
|
|
private PowerManager.WakeLock wakeLock;
|
|
|
|
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();
|
|
}
|
|
|
|
@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());
|
|
|
|
playback = new MultiPlayer(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));
|
|
|
|
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);
|
|
|
|
PreferenceUtil.getInstance(this).registerOnSharedPreferenceChangedListener(this);
|
|
|
|
restoreState();
|
|
|
|
sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_CREATED"));
|
|
|
|
registerHeadsetEvents();
|
|
registerBluetoothConnected();
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
unregisterReceiver(widgetIntentReceiver);
|
|
unregisterReceiver(updateFavoriteReceiver);
|
|
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.getInstance(this).unregisterOnSharedPreferenceChangedListener(this);
|
|
wakeLock.release();
|
|
|
|
sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_DESTROYED"));
|
|
}
|
|
|
|
public void acquireWakeLock(long milli) {
|
|
wakeLock.acquire(milli);
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
@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;
|
|
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:
|
|
updateNotification();
|
|
break;
|
|
case PreferenceUtil.CLASSIC_NOTIFICATION:
|
|
initNotification();
|
|
updateNotification();
|
|
break;
|
|
case PreferenceUtil.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.toggleFavorite(getApplicationContext(), getCurrentSong());
|
|
break;
|
|
}
|
|
}
|
|
|
|
return START_NOT_STICKY;
|
|
}
|
|
|
|
@Override
|
|
public void onTrackEnded() {
|
|
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() {
|
|
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 {
|
|
playback.start();
|
|
if (!becomingNoisyReceiverRegistered) {
|
|
registerReceiver(becomingNoisyReceiver, becomingNoisyReceiverIntentFilter);
|
|
becomingNoisyReceiverRegistered = true;
|
|
}
|
|
if (notHandledMetaChangedForCurrentTrack) {
|
|
handleChangeInternal(META_CHANGED);
|
|
notHandledMetaChangedForCurrentTrack = false;
|
|
}
|
|
notifyChange(PLAY_STATE_CHANGED);
|
|
|
|
// fixes a bug where the volume would stay ducked because the AudioManager.AUDIOFOCUS_GAIN event is not sent
|
|
playerHandler.removeMessages(DUCK);
|
|
playerHandler.sendEmptyMessage(UNDUCK);
|
|
}
|
|
}
|
|
} else {
|
|
Toast.makeText(this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT)
|
|
.show();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void 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<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 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 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 {
|
|
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);
|
|
}
|
|
}
|
|
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()) {
|
|
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) {
|
|
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<?, Bitmap> 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<Bitmap>(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<? super Bitmap> 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) {
|
|
switch (what) {
|
|
case PLAY_STATE_CHANGED:
|
|
updateNotification();
|
|
updateMediaSessionPlaybackState();
|
|
final boolean isPlaying = isPlaying();
|
|
if (!isPlaying && getSongProgressMillis() > 0) {
|
|
savePositionInTrack();
|
|
}
|
|
songPlayCountHelper.notifyPlayStateChanged(isPlaying);
|
|
break;
|
|
case FAVORITE_STATE_CHANGED:
|
|
case META_CHANGED:
|
|
updateNotification();
|
|
updateMediaSessionMetaData();
|
|
savePosition();
|
|
savePositionInTrack();
|
|
final Song currentSong = getCurrentSong();
|
|
if (currentSong != null) {
|
|
HistoryStore.getInstance(this).addSongId(currentSong.getId());
|
|
}
|
|
if (songPlayCountHelper.shouldBumpPlayCount()) {
|
|
SongPlayCountStore.getInstance(this).bumpPlayCount(songPlayCountHelper.getSong().getId());
|
|
}
|
|
if (currentSong != null) {
|
|
songPlayCountHelper.notifySongChanged(currentSong);
|
|
}
|
|
break;
|
|
case QUEUE_CHANGED:
|
|
updateMediaSessionMetaData(); // because playing queue size might have changed
|
|
saveState();
|
|
if (playingQueue.size() > 0) {
|
|
prepareNext();
|
|
} else {
|
|
playingNotification.stop();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 playFromPlaylist(Intent intent) {
|
|
Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST);
|
|
int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode());
|
|
if (playlist != null) {
|
|
ArrayList<Song> 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void registerBluetoothConnected() {
|
|
Log.i(TAG, "registerBluetoothConnected: ");
|
|
if (!bluetoothConnectedRegistered) {
|
|
registerReceiver(bluetoothReceiver, bluetoothConnectedIntentFilter);
|
|
bluetoothConnectedRegistered = true;
|
|
}
|
|
}
|
|
|
|
private void registerHeadsetEvents() {
|
|
if (!headsetReceiverRegistered && PreferenceUtil.getInstance(this).getHeadsetPlugged()) {
|
|
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);
|
|
}
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
public class MusicBinder extends Binder {
|
|
|
|
@NonNull
|
|
public MusicService getService() {
|
|
return MusicService.this;
|
|
}
|
|
}
|
|
} |