Moved MediaSession handling to its own class
Fixed queue and position handling on Now Playing screen
This commit is contained in:
parent
83c6b76d0a
commit
56af9e4bf2
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.AutoMediaBrowserService"
|
android:name=".service.AutoMediaBrowserService"
|
||||||
android:label="Ultrasonic Auto Media Player Service"
|
android:label="@string/common.appname"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.moire.ultrasonic.service.Consumer;
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController;
|
import org.moire.ultrasonic.service.MediaPlayerController;
|
||||||
import org.moire.ultrasonic.util.Constants;
|
import org.moire.ultrasonic.util.Constants;
|
||||||
import org.moire.ultrasonic.util.FileUtil;
|
import org.moire.ultrasonic.util.FileUtil;
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionHandler;
|
||||||
import org.moire.ultrasonic.util.PermissionUtil;
|
import org.moire.ultrasonic.util.PermissionUtil;
|
||||||
import org.moire.ultrasonic.util.ThemeChangedEventDistributor;
|
import org.moire.ultrasonic.util.ThemeChangedEventDistributor;
|
||||||
import org.moire.ultrasonic.util.TimeSpanPreference;
|
import org.moire.ultrasonic.util.TimeSpanPreference;
|
||||||
|
@ -89,6 +90,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||||
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
|
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
|
||||||
private final Lazy<PermissionUtil> permissionUtil = inject(PermissionUtil.class);
|
private final Lazy<PermissionUtil> permissionUtil = inject(PermissionUtil.class);
|
||||||
private final Lazy<ThemeChangedEventDistributor> themeChangedEventDistributor = inject(ThemeChangedEventDistributor.class);
|
private final Lazy<ThemeChangedEventDistributor> themeChangedEventDistributor = inject(ThemeChangedEventDistributor.class);
|
||||||
|
private final Lazy<MediaSessionHandler> mediaSessionHandler = inject(MediaSessionHandler.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
@ -468,7 +470,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||||
|
|
||||||
private void setMediaButtonsEnabled(boolean enabled) {
|
private void setMediaButtonsEnabled(boolean enabled) {
|
||||||
lockScreenEnabled.setEnabled(enabled);
|
lockScreenEnabled.setEnabled(enabled);
|
||||||
Util.updateMediaButtonEventReceiver();
|
mediaSessionHandler.getValue().updateMediaButtonReceiver();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setBluetoothPreferences(boolean enabled) {
|
private void setBluetoothPreferences(boolean enabled) {
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
package org.moire.ultrasonic.service;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.util.Constants;
|
|
||||||
import org.moire.ultrasonic.util.FileUtil;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is responsible for the serialization / deserialization
|
|
||||||
* of the DownloadQueue (playlist) to the filesystem.
|
|
||||||
* It also serializes the player state e.g. current playing number and play position.
|
|
||||||
*/
|
|
||||||
public class DownloadQueueSerializer
|
|
||||||
{
|
|
||||||
public final Lock lock = new ReentrantLock();
|
|
||||||
public final AtomicBoolean setup = new AtomicBoolean(false);
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
public DownloadQueueSerializer(Context context)
|
|
||||||
{
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void serializeDownloadQueue(Iterable<DownloadFile> songs, int currentPlayingIndex, int currentPlayingPosition)
|
|
||||||
{
|
|
||||||
if (!setup.get())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
new SerializeTask().execute(songs, currentPlayingIndex, currentPlayingPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void serializeDownloadQueueNow(Iterable<DownloadFile> songs, int currentPlayingIndex, int currentPlayingPosition)
|
|
||||||
{
|
|
||||||
State state = new State();
|
|
||||||
for (DownloadFile downloadFile : songs)
|
|
||||||
{
|
|
||||||
state.songs.add(downloadFile.getSong());
|
|
||||||
}
|
|
||||||
state.currentPlayingIndex = currentPlayingIndex;
|
|
||||||
state.currentPlayingPosition = currentPlayingPosition;
|
|
||||||
|
|
||||||
Timber.i("Serialized currentPlayingIndex: %d, currentPlayingPosition: %d", state.currentPlayingIndex, state.currentPlayingPosition);
|
|
||||||
FileUtil.serialize(context, state, Constants.FILENAME_DOWNLOADS_SER);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deserializeDownloadQueue(Consumer<State> afterDeserialized)
|
|
||||||
{
|
|
||||||
new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, afterDeserialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deserializeDownloadQueueNow(Consumer<State> afterDeserialized)
|
|
||||||
{
|
|
||||||
State state = FileUtil.deserialize(context, Constants.FILENAME_DOWNLOADS_SER);
|
|
||||||
if (state == null) return;
|
|
||||||
Timber.i("Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition);
|
|
||||||
afterDeserialized.accept(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SerializeTask extends AsyncTask<Object, Void, Void>
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Object... params)
|
|
||||||
{
|
|
||||||
if (lock.tryLock())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Thread.currentThread().setName("SerializeTask");
|
|
||||||
serializeDownloadQueueNow((Iterable<DownloadFile>)params[0], (int)params[1], (int)params[2]);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DeserializeTask extends AsyncTask<Object, Void, Void>
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Object... params)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Thread.currentThread().setName("DeserializeTask");
|
|
||||||
lock.lock();
|
|
||||||
deserializeDownloadQueueNow((Consumer<State>)params[0]);
|
|
||||||
setup.set(true);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,308 +0,0 @@
|
||||||
/*
|
|
||||||
This file is part of Subsonic.
|
|
||||||
|
|
||||||
Subsonic 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.
|
|
||||||
|
|
||||||
Subsonic 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.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Copyright 2009 (C) Sindre Mehus
|
|
||||||
*/
|
|
||||||
package org.moire.ultrasonic.service;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.media.AudioManager;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import timber.log.Timber;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
|
||||||
import org.moire.ultrasonic.app.UApp;
|
|
||||||
import org.moire.ultrasonic.domain.PlayerState;
|
|
||||||
import org.moire.ultrasonic.util.CacheCleaner;
|
|
||||||
import org.moire.ultrasonic.util.Constants;
|
|
||||||
import org.moire.ultrasonic.util.Util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is responsible for handling received events for the Media Player implementation
|
|
||||||
*
|
|
||||||
* @author Sindre Mehus
|
|
||||||
*/
|
|
||||||
public class MediaPlayerLifecycleSupport
|
|
||||||
{
|
|
||||||
private boolean created = false;
|
|
||||||
private final DownloadQueueSerializer downloadQueueSerializer; // From DI
|
|
||||||
private final MediaPlayerController mediaPlayerController; // From DI
|
|
||||||
private final Downloader downloader; // From DI
|
|
||||||
|
|
||||||
private BroadcastReceiver headsetEventReceiver;
|
|
||||||
|
|
||||||
public MediaPlayerLifecycleSupport(DownloadQueueSerializer downloadQueueSerializer,
|
|
||||||
final MediaPlayerController mediaPlayerController, final Downloader downloader)
|
|
||||||
{
|
|
||||||
this.downloadQueueSerializer = downloadQueueSerializer;
|
|
||||||
this.mediaPlayerController = mediaPlayerController;
|
|
||||||
this.downloader = downloader;
|
|
||||||
|
|
||||||
Timber.i("LifecycleSupport constructed");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCreate()
|
|
||||||
{
|
|
||||||
onCreate(false, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onCreate(final boolean autoPlay, final Runnable afterCreated)
|
|
||||||
{
|
|
||||||
if (created)
|
|
||||||
{
|
|
||||||
if (afterCreated != null) afterCreated.run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerHeadsetReceiver();
|
|
||||||
|
|
||||||
mediaPlayerController.onCreate();
|
|
||||||
if (autoPlay) mediaPlayerController.preload();
|
|
||||||
|
|
||||||
this.downloadQueueSerializer.deserializeDownloadQueue(new Consumer<State>() {
|
|
||||||
@Override
|
|
||||||
public void accept(State state) {
|
|
||||||
mediaPlayerController.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition, autoPlay, false);
|
|
||||||
|
|
||||||
// Work-around: Serialize again, as the restore() method creates a serialization without current playing info.
|
|
||||||
MediaPlayerLifecycleSupport.this.downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList,
|
|
||||||
downloader.getCurrentPlayingIndex(), mediaPlayerController.getPlayerPosition());
|
|
||||||
|
|
||||||
if (afterCreated != null) afterCreated.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
new CacheCleaner().clean();
|
|
||||||
created = true;
|
|
||||||
Timber.i("LifecycleSupport created");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onDestroy()
|
|
||||||
{
|
|
||||||
if (!created) return;
|
|
||||||
downloadQueueSerializer.serializeDownloadQueueNow(downloader.downloadList,
|
|
||||||
downloader.getCurrentPlayingIndex(), mediaPlayerController.getPlayerPosition());
|
|
||||||
mediaPlayerController.clear(false);
|
|
||||||
UApp.Companion.applicationContext().unregisterReceiver(headsetEventReceiver);
|
|
||||||
mediaPlayerController.onDestroy();
|
|
||||||
created = false;
|
|
||||||
Timber.i("LifecycleSupport destroyed");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void receiveIntent(Intent intent)
|
|
||||||
{
|
|
||||||
if (intent == null) return;
|
|
||||||
String intentAction = intent.getAction();
|
|
||||||
if (intentAction == null || intentAction.isEmpty()) return;
|
|
||||||
|
|
||||||
Timber.i("Received intent: %s", intentAction);
|
|
||||||
|
|
||||||
if (intentAction.equals(Constants.CMD_PROCESS_KEYCODE)) {
|
|
||||||
if (intent.getExtras() != null) {
|
|
||||||
KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
|
|
||||||
if (event != null) {
|
|
||||||
handleKeyEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
handleUltrasonicIntent(intentAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Headset Intent Receiver is responsible for resuming playback when a headset is inserted
|
|
||||||
* and pausing it when it is removed.
|
|
||||||
* Unfortunately this Intent can't be registered in the AndroidManifest, so it works only
|
|
||||||
* while Ultrasonic is running.
|
|
||||||
*/
|
|
||||||
private void registerHeadsetReceiver() {
|
|
||||||
final SharedPreferences sp = Util.getPreferences();
|
|
||||||
final Context context = UApp.Companion.applicationContext();
|
|
||||||
final String spKey = context
|
|
||||||
.getString(R.string.settings_playback_resume_play_on_headphones_plug);
|
|
||||||
|
|
||||||
headsetEventReceiver = new BroadcastReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
final Bundle extras = intent.getExtras();
|
|
||||||
|
|
||||||
if (extras == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.i("Headset event for: %s", extras.get("name"));
|
|
||||||
final int state = extras.getInt("state");
|
|
||||||
if (state == 0) {
|
|
||||||
if (!mediaPlayerController.isJukeboxEnabled()) {
|
|
||||||
mediaPlayerController.pause();
|
|
||||||
}
|
|
||||||
} else if (state == 1) {
|
|
||||||
if (!mediaPlayerController.isJukeboxEnabled() &&
|
|
||||||
sp.getBoolean(spKey, false) &&
|
|
||||||
mediaPlayerController.getPlayerState() == PlayerState.PAUSED) {
|
|
||||||
mediaPlayerController.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
IntentFilter headsetIntentFilter;
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
{
|
|
||||||
headsetIntentFilter = new IntentFilter(AudioManager.ACTION_HEADSET_PLUG);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
headsetIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
|
|
||||||
}
|
|
||||||
UApp.Companion.applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleKeyEvent(KeyEvent event)
|
|
||||||
{
|
|
||||||
if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int keyCode;
|
|
||||||
int receivedKeyCode = event.getKeyCode();
|
|
||||||
// Translate PLAY and PAUSE codes to PLAY_PAUSE to improve compatibility with old Bluetooth devices
|
|
||||||
if (Util.getSingleButtonPlayPause() &&
|
|
||||||
(receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
|
|
||||||
receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PAUSE)) {
|
|
||||||
Timber.i("Single button Play/Pause is set, rewriting keyCode to PLAY_PAUSE");
|
|
||||||
keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
|
|
||||||
}
|
|
||||||
else keyCode = receivedKeyCode;
|
|
||||||
|
|
||||||
boolean autoStart = (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
|
|
||||||
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
|
|
||||||
keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
|
|
||||||
keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS ||
|
|
||||||
keyCode == KeyEvent.KEYCODE_MEDIA_NEXT);
|
|
||||||
|
|
||||||
// We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start
|
|
||||||
onCreate(autoStart, () -> {
|
|
||||||
switch (keyCode)
|
|
||||||
{
|
|
||||||
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
|
|
||||||
case KeyEvent.KEYCODE_HEADSETHOOK:
|
|
||||||
mediaPlayerController.togglePlayPause();
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
|
|
||||||
mediaPlayerController.previous();
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_MEDIA_NEXT:
|
|
||||||
mediaPlayerController.next();
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_MEDIA_STOP:
|
|
||||||
mediaPlayerController.stop();
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_MEDIA_PLAY:
|
|
||||||
if (mediaPlayerController.getPlayerState() == PlayerState.IDLE)
|
|
||||||
{
|
|
||||||
mediaPlayerController.play();
|
|
||||||
}
|
|
||||||
else if (mediaPlayerController.getPlayerState() != PlayerState.STARTED)
|
|
||||||
{
|
|
||||||
mediaPlayerController.start();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_MEDIA_PAUSE:
|
|
||||||
mediaPlayerController.pause();
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_1:
|
|
||||||
mediaPlayerController.setSongRating(1);
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_2:
|
|
||||||
mediaPlayerController.setSongRating(2);
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_3:
|
|
||||||
mediaPlayerController.setSongRating(3);
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_4:
|
|
||||||
mediaPlayerController.setSongRating(4);
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_5:
|
|
||||||
mediaPlayerController.setSongRating(5);
|
|
||||||
break;
|
|
||||||
case KeyEvent.KEYCODE_STAR:
|
|
||||||
mediaPlayerController.toggleSongStarred();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function processes the intent that could come from other applications.
|
|
||||||
*/
|
|
||||||
private void handleUltrasonicIntent(final String intentAction)
|
|
||||||
{
|
|
||||||
final boolean isRunning = created;
|
|
||||||
// If Ultrasonic is not running, do nothing to stop or pause
|
|
||||||
if (!isRunning && (intentAction.equals(Constants.CMD_PAUSE) ||
|
|
||||||
intentAction.equals(Constants.CMD_STOP))) return;
|
|
||||||
|
|
||||||
boolean autoStart = (intentAction.equals(Constants.CMD_PLAY) ||
|
|
||||||
intentAction.equals(Constants.CMD_RESUME_OR_PLAY) ||
|
|
||||||
intentAction.equals(Constants.CMD_TOGGLEPAUSE) ||
|
|
||||||
intentAction.equals(Constants.CMD_PREVIOUS) ||
|
|
||||||
intentAction.equals(Constants.CMD_NEXT));
|
|
||||||
|
|
||||||
// We can receive intents when everything is stopped, so we need to start
|
|
||||||
onCreate(autoStart, () -> {
|
|
||||||
switch(intentAction)
|
|
||||||
{
|
|
||||||
case Constants.CMD_PLAY:
|
|
||||||
mediaPlayerController.play();
|
|
||||||
break;
|
|
||||||
case Constants.CMD_RESUME_OR_PLAY:
|
|
||||||
// If Ultrasonic wasn't running, the autoStart is enough to resume, no need to call anything
|
|
||||||
if (isRunning) mediaPlayerController.resumeOrPlay();
|
|
||||||
break;
|
|
||||||
case Constants.CMD_NEXT:
|
|
||||||
mediaPlayerController.next();
|
|
||||||
break;
|
|
||||||
case Constants.CMD_PREVIOUS:
|
|
||||||
mediaPlayerController.previous();
|
|
||||||
break;
|
|
||||||
case Constants.CMD_TOGGLEPAUSE:
|
|
||||||
mediaPlayerController.togglePlayPause();
|
|
||||||
break;
|
|
||||||
case Constants.CMD_STOP:
|
|
||||||
// TODO: There is a stop() function, shouldn't we use that?
|
|
||||||
mediaPlayerController.pause();
|
|
||||||
mediaPlayerController.seekTo(0);
|
|
||||||
break;
|
|
||||||
case Constants.CMD_PAUSE:
|
|
||||||
mediaPlayerController.pause();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -692,16 +692,6 @@ public class Util
|
||||||
return Bitmap.createScaledBitmap(bitmap, size, getScaledHeight(bitmap, size), true);
|
return Bitmap.createScaledBitmap(bitmap, size, getScaledHeight(bitmap, size), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger an update on the MediaSession. Depending on the preference it will register
|
|
||||||
// or deregister the MediaButtonReceiver.
|
|
||||||
public static void updateMediaButtonEventReceiver()
|
|
||||||
{
|
|
||||||
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
|
|
||||||
if (mediaPlayerService != null) {
|
|
||||||
mediaPlayerService.updateMediaButtonReceiver();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MusicDirectory getSongsFromSearchResult(SearchResult searchResult)
|
public static MusicDirectory getSongsFromSearchResult(SearchResult searchResult)
|
||||||
{
|
{
|
||||||
MusicDirectory musicDirectory = new MusicDirectory();
|
MusicDirectory musicDirectory = new MusicDirectory();
|
||||||
|
|
|
@ -5,6 +5,7 @@ import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionHandler
|
||||||
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
||||||
import org.moire.ultrasonic.util.PermissionUtil
|
import org.moire.ultrasonic.util.PermissionUtil
|
||||||
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
||||||
|
@ -19,4 +20,5 @@ val applicationModule = module {
|
||||||
single { NowPlayingEventDistributor() }
|
single { NowPlayingEventDistributor() }
|
||||||
single { ThemeChangedEventDistributor() }
|
single { ThemeChangedEventDistributor() }
|
||||||
single { MediaSessionEventDistributor() }
|
single { MediaSessionEventDistributor() }
|
||||||
|
single { MediaSessionHandler() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,12 @@ import org.moire.ultrasonic.util.ShufflePlayBuffer
|
||||||
*/
|
*/
|
||||||
val mediaPlayerModule = module {
|
val mediaPlayerModule = module {
|
||||||
single { JukeboxMediaPlayer(get()) }
|
single { JukeboxMediaPlayer(get()) }
|
||||||
single { MediaPlayerLifecycleSupport(get(), get(), get()) }
|
single { MediaPlayerLifecycleSupport() }
|
||||||
single { DownloadQueueSerializer(androidContext()) }
|
single { DownloadQueueSerializer() }
|
||||||
single { ExternalStorageMonitor() }
|
single { ExternalStorageMonitor() }
|
||||||
single { ShufflePlayBuffer() }
|
single { ShufflePlayBuffer() }
|
||||||
single { Downloader(get(), get(), get()) }
|
single { Downloader(get(), get(), get()) }
|
||||||
single { LocalMediaPlayer(get(), androidContext()) }
|
single { LocalMediaPlayer() }
|
||||||
single { AudioFocusHandler(get()) }
|
single { AudioFocusHandler(get()) }
|
||||||
|
|
||||||
// TODO Ideally this can be cleaned up when all circular references are removed.
|
// TODO Ideally this can be cleaned up when all circular references are removed.
|
||||||
|
|
|
@ -10,6 +10,7 @@ import androidx.media.utils.MediaConstants
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
||||||
import org.moire.ultrasonic.util.MediaSessionEventListener
|
import org.moire.ultrasonic.util.MediaSessionEventListener
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionHandler
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,49 +24,49 @@ const val MY_MEDIA_PLAYLIST_ID = "MY_MEDIA_PLAYLIST_ID"
|
||||||
class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
||||||
private val mediaSessionEventDistributor: MediaSessionEventDistributor by inject()
|
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
|
||||||
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
|
private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
|
||||||
|
private val mediaSessionHandler by inject<MediaSessionHandler>()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
mediaSessionEventListener = object : MediaSessionEventListener {
|
mediaSessionEventListener = object : MediaSessionEventListener {
|
||||||
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
|
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
|
||||||
Timber.i("AutoMediaBrowserService onMediaSessionTokenCreated called")
|
|
||||||
if (sessionToken == null) {
|
if (sessionToken == null) {
|
||||||
Timber.i("AutoMediaBrowserService onMediaSessionTokenCreated session token was null, set it to %s", token.toString())
|
|
||||||
sessionToken = token
|
sessionToken = token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {
|
||||||
// TODO implement
|
// TODO implement
|
||||||
Timber.i("AutoMediaBrowserService onPlayFromMediaIdRequested called")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {
|
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {
|
||||||
// TODO implement
|
// TODO implement
|
||||||
Timber.i("AutoMediaBrowserService onPlayFromSearchRequested called")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
||||||
|
mediaSessionHandler.initialize()
|
||||||
|
|
||||||
val handler = Handler()
|
val handler = Handler()
|
||||||
handler.postDelayed({
|
handler.postDelayed({
|
||||||
Timber.i("AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService...")
|
// Ultrasonic may be started from Android Auto. This boots up the necessary components.
|
||||||
// TODO it seems Android Auto handles autostart, but we must check that
|
Timber.d("AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService...")
|
||||||
lifecycleSupport.onCreate()
|
lifecycleSupport.onCreate()
|
||||||
MediaPlayerService.getInstance()
|
MediaPlayerService.getInstance()
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
Timber.i("AutoMediaBrowserService onCreate called")
|
Timber.i("AutoMediaBrowserService onCreate finished")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
||||||
Timber.i("AutoMediaBrowserService onDestroy called")
|
mediaSessionHandler.release()
|
||||||
|
|
||||||
|
Timber.i("AutoMediaBrowserService onDestroy finished")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetRoot(
|
override fun onGetRoot(
|
||||||
|
@ -73,7 +74,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||||
clientUid: Int,
|
clientUid: Int,
|
||||||
rootHints: Bundle?
|
rootHints: Bundle?
|
||||||
): BrowserRoot? {
|
): BrowserRoot? {
|
||||||
Timber.i("AutoMediaBrowserService onGetRoot called")
|
Timber.d("AutoMediaBrowserService onGetRoot called")
|
||||||
|
|
||||||
// TODO: The number of horizontal items available on the Andoid Auto screen. Check and handle.
|
// TODO: The number of horizontal items available on the Andoid Auto screen. Check and handle.
|
||||||
val maximumRootChildLimit = rootHints!!.getInt(
|
val maximumRootChildLimit = rootHints!!.getInt(
|
||||||
|
@ -102,7 +103,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||||
parentId: String,
|
parentId: String,
|
||||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
||||||
) {
|
) {
|
||||||
Timber.i("AutoMediaBrowserService onLoadChildren called")
|
Timber.d("AutoMediaBrowserService onLoadChildren called")
|
||||||
|
|
||||||
if (parentId == MY_MEDIA_ROOT_ID) {
|
if (parentId == MY_MEDIA_ROOT_ID) {
|
||||||
return getRootItems(result)
|
return getRootItems(result)
|
||||||
|
|
|
@ -142,8 +142,8 @@ class AutoMediaPlayerService: MediaBrowserServiceCompat() {
|
||||||
albumListModel = AlbumListModel(application)
|
albumListModel = AlbumListModel(application)
|
||||||
artistListModel = ArtistListModel(application)
|
artistListModel = ArtistListModel(application)
|
||||||
|
|
||||||
mediaPlayerService.onCreate()
|
//mediaPlayerService.onCreate()
|
||||||
mediaPlayerService.updateMediaSession(null, PlayerState.IDLE)
|
//mediaPlayerService.updateMediaSession(null, PlayerState.IDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
|
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionHandler
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.locks.Lock
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for the serialization / deserialization
|
||||||
|
* of the DownloadQueue (playlist) to the filesystem.
|
||||||
|
* It also serializes the player state e.g. current playing number and play position.
|
||||||
|
*/
|
||||||
|
class DownloadQueueSerializer : KoinComponent {
|
||||||
|
|
||||||
|
private val context by inject<Context>()
|
||||||
|
private val mediaSessionHandler by inject<MediaSessionHandler>()
|
||||||
|
|
||||||
|
val lock: Lock = ReentrantLock()
|
||||||
|
val setup = AtomicBoolean(false)
|
||||||
|
|
||||||
|
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
fun serializeDownloadQueue(
|
||||||
|
songs: Iterable<DownloadFile>,
|
||||||
|
currentPlayingIndex: Int,
|
||||||
|
currentPlayingPosition: Int
|
||||||
|
) {
|
||||||
|
if (!setup.get()) return
|
||||||
|
|
||||||
|
appScope.launch {
|
||||||
|
if (lock.tryLock()) {
|
||||||
|
try {
|
||||||
|
serializeDownloadQueueNow(songs, currentPlayingIndex, currentPlayingPosition)
|
||||||
|
} finally {
|
||||||
|
lock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serializeDownloadQueueNow(
|
||||||
|
songs: Iterable<DownloadFile>,
|
||||||
|
currentPlayingIndex: Int,
|
||||||
|
currentPlayingPosition: Int
|
||||||
|
) {
|
||||||
|
val state = State()
|
||||||
|
|
||||||
|
for (downloadFile in songs) {
|
||||||
|
state.songs.add(downloadFile.song)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.currentPlayingIndex = currentPlayingIndex
|
||||||
|
state.currentPlayingPosition = currentPlayingPosition
|
||||||
|
|
||||||
|
Timber.i(
|
||||||
|
"Serialized currentPlayingIndex: %d, currentPlayingPosition: %d",
|
||||||
|
state.currentPlayingIndex,
|
||||||
|
state.currentPlayingPosition
|
||||||
|
)
|
||||||
|
|
||||||
|
FileUtil.serialize(context, state, Constants.FILENAME_DOWNLOADS_SER)
|
||||||
|
|
||||||
|
// This is called here because the queue is usually serialized after a change
|
||||||
|
mediaSessionHandler.updateMediaSessionQueue(state.songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserializeDownloadQueue(afterDeserialized: Consumer<State?>) {
|
||||||
|
|
||||||
|
appScope.launch {
|
||||||
|
try {
|
||||||
|
lock.lock()
|
||||||
|
deserializeDownloadQueueNow(afterDeserialized)
|
||||||
|
setup.set(true)
|
||||||
|
} finally {
|
||||||
|
lock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deserializeDownloadQueueNow(afterDeserialized: Consumer<State?>) {
|
||||||
|
|
||||||
|
val state = FileUtil.deserialize<State>(
|
||||||
|
context, Constants.FILENAME_DOWNLOADS_SER
|
||||||
|
) ?: return
|
||||||
|
|
||||||
|
Timber.i(
|
||||||
|
"Deserialized currentPlayingIndex: %d, currentPlayingPosition: %d ",
|
||||||
|
state.currentPlayingIndex,
|
||||||
|
state.currentPlayingPosition
|
||||||
|
)
|
||||||
|
|
||||||
|
mediaSessionHandler.updateMediaSessionQueue(state.songs)
|
||||||
|
afterDeserialized.accept(state)
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ import android.os.PowerManager
|
||||||
import android.os.PowerManager.PARTIAL_WAKE_LOCK
|
import android.os.PowerManager.PARTIAL_WAKE_LOCK
|
||||||
import android.os.PowerManager.WakeLock
|
import android.os.PowerManager.WakeLock
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -32,6 +34,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.domain.PlayerState
|
import org.moire.ultrasonic.domain.PlayerState
|
||||||
import org.moire.ultrasonic.util.CancellableTask
|
import org.moire.ultrasonic.util.CancellableTask
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionHandler
|
||||||
import org.moire.ultrasonic.util.StreamProxy
|
import org.moire.ultrasonic.util.StreamProxy
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -39,10 +42,11 @@ import timber.log.Timber
|
||||||
/**
|
/**
|
||||||
* Represents a Media Player which uses the mobile's resources for playback
|
* Represents a Media Player which uses the mobile's resources for playback
|
||||||
*/
|
*/
|
||||||
class LocalMediaPlayer(
|
class LocalMediaPlayer: KoinComponent {
|
||||||
private val audioFocusHandler: AudioFocusHandler,
|
|
||||||
private val context: Context
|
private val audioFocusHandler by inject<AudioFocusHandler>()
|
||||||
) {
|
private val context by inject<Context>()
|
||||||
|
private val mediaSessionHandler by inject<MediaSessionHandler>()
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null
|
var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null
|
||||||
|
@ -705,8 +709,11 @@ class LocalMediaPlayer(
|
||||||
try {
|
try {
|
||||||
if (playerState === PlayerState.STARTED) {
|
if (playerState === PlayerState.STARTED) {
|
||||||
cachedPosition = mediaPlayer.currentPosition
|
cachedPosition = mediaPlayer.currentPosition
|
||||||
|
mediaSessionHandler.updateMediaSessionPlaybackPosition(
|
||||||
|
cachedPosition.toLong()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Util.sleepQuietly(50L)
|
Util.sleepQuietly(100L)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.w(e, "Crashed getting current position")
|
Timber.w(e, "Crashed getting current position")
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
|
|
@ -0,0 +1,280 @@
|
||||||
|
/*
|
||||||
|
This file is part of Subsonic.
|
||||||
|
|
||||||
|
Subsonic 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.
|
||||||
|
|
||||||
|
Subsonic 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.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2009 (C) Sindre Mehus
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import kotlinx.coroutines.newFixedThreadPoolContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||||
|
import org.moire.ultrasonic.domain.PlayerState
|
||||||
|
import org.moire.ultrasonic.util.CacheCleaner
|
||||||
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionEventListener
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for handling received events for the Media Player implementation
|
||||||
|
*
|
||||||
|
* @author Sindre Mehus
|
||||||
|
*/
|
||||||
|
class MediaPlayerLifecycleSupport : KoinComponent {
|
||||||
|
private val downloadQueueSerializer by inject<DownloadQueueSerializer>()
|
||||||
|
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||||
|
private val downloader by inject<Downloader>()
|
||||||
|
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
|
||||||
|
|
||||||
|
private var created = false
|
||||||
|
private var headsetEventReceiver: BroadcastReceiver? = null
|
||||||
|
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
||||||
|
|
||||||
|
fun onCreate() {
|
||||||
|
onCreate(false, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCreate(autoPlay: Boolean, afterCreated: Runnable?) {
|
||||||
|
|
||||||
|
if (created) {
|
||||||
|
afterCreated?.run()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSessionEventListener = object : MediaSessionEventListener {
|
||||||
|
override fun onMediaButtonEvent(keyEvent: KeyEvent?) {
|
||||||
|
if (keyEvent != null) handleKeyEvent(keyEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
||||||
|
registerHeadsetReceiver()
|
||||||
|
mediaPlayerController.onCreate()
|
||||||
|
if (autoPlay) mediaPlayerController.preload()
|
||||||
|
|
||||||
|
downloadQueueSerializer.deserializeDownloadQueue(object : Consumer<State?>() {
|
||||||
|
override fun accept(state: State?) {
|
||||||
|
mediaPlayerController.restore(
|
||||||
|
state!!.songs,
|
||||||
|
state.currentPlayingIndex,
|
||||||
|
state.currentPlayingPosition,
|
||||||
|
autoPlay,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Work-around: Serialize again, as the restore() method creates a serialization without current playing info.
|
||||||
|
downloadQueueSerializer.serializeDownloadQueue(
|
||||||
|
downloader.downloadList,
|
||||||
|
downloader.currentPlayingIndex,
|
||||||
|
mediaPlayerController.playerPosition
|
||||||
|
)
|
||||||
|
afterCreated?.run()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
CacheCleaner().clean()
|
||||||
|
created = true
|
||||||
|
Timber.i("LifecycleSupport created")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDestroy() {
|
||||||
|
|
||||||
|
if (!created) return
|
||||||
|
|
||||||
|
downloadQueueSerializer.serializeDownloadQueueNow(
|
||||||
|
downloader.downloadList,
|
||||||
|
downloader.currentPlayingIndex,
|
||||||
|
mediaPlayerController.playerPosition
|
||||||
|
)
|
||||||
|
|
||||||
|
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
||||||
|
|
||||||
|
mediaPlayerController.clear(false)
|
||||||
|
applicationContext().unregisterReceiver(headsetEventReceiver)
|
||||||
|
mediaPlayerController.onDestroy()
|
||||||
|
|
||||||
|
created = false
|
||||||
|
Timber.i("LifecycleSupport destroyed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun receiveIntent(intent: Intent?) {
|
||||||
|
|
||||||
|
if (intent == null) return
|
||||||
|
|
||||||
|
val intentAction = intent.action
|
||||||
|
if (intentAction == null || intentAction.isEmpty()) return
|
||||||
|
|
||||||
|
Timber.i("Received intent: %s", intentAction)
|
||||||
|
|
||||||
|
if (intentAction == Constants.CMD_PROCESS_KEYCODE) {
|
||||||
|
if (intent.extras != null) {
|
||||||
|
val event = intent.extras!![Intent.EXTRA_KEY_EVENT] as KeyEvent?
|
||||||
|
event?.let { handleKeyEvent(it) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleUltrasonicIntent(intentAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Headset Intent Receiver is responsible for resuming playback when a headset is inserted
|
||||||
|
* and pausing it when it is removed.
|
||||||
|
* Unfortunately this Intent can't be registered in the AndroidManifest, so it works only
|
||||||
|
* while Ultrasonic is running.
|
||||||
|
*/
|
||||||
|
private fun registerHeadsetReceiver() {
|
||||||
|
|
||||||
|
val sp = Util.getPreferences()
|
||||||
|
val context = applicationContext()
|
||||||
|
val spKey = context
|
||||||
|
.getString(R.string.settings_playback_resume_play_on_headphones_plug)
|
||||||
|
|
||||||
|
headsetEventReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val extras = intent.extras ?: return
|
||||||
|
|
||||||
|
Timber.i("Headset event for: %s", extras["name"])
|
||||||
|
|
||||||
|
val state = extras.getInt("state")
|
||||||
|
|
||||||
|
if (state == 0) {
|
||||||
|
if (!mediaPlayerController.isJukeboxEnabled) {
|
||||||
|
mediaPlayerController.pause()
|
||||||
|
}
|
||||||
|
} else if (state == 1) {
|
||||||
|
if (!mediaPlayerController.isJukeboxEnabled &&
|
||||||
|
sp.getBoolean(
|
||||||
|
spKey,
|
||||||
|
false
|
||||||
|
) && mediaPlayerController.playerState === PlayerState.PAUSED
|
||||||
|
) {
|
||||||
|
mediaPlayerController.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val headsetIntentFilter: IntentFilter =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
IntentFilter(AudioManager.ACTION_HEADSET_PLUG)
|
||||||
|
} else {
|
||||||
|
IntentFilter(Intent.ACTION_HEADSET_PLUG)
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleKeyEvent(event: KeyEvent) {
|
||||||
|
|
||||||
|
if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return
|
||||||
|
|
||||||
|
val keyCode: Int
|
||||||
|
val receivedKeyCode = event.keyCode
|
||||||
|
|
||||||
|
// Translate PLAY and PAUSE codes to PLAY_PAUSE to improve compatibility with old Bluetooth devices
|
||||||
|
keyCode = if (Util.getSingleButtonPlayPause() &&
|
||||||
|
(receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
|
||||||
|
receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PAUSE)
|
||||||
|
) {
|
||||||
|
Timber.i("Single button Play/Pause is set, rewriting keyCode to PLAY_PAUSE")
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||||
|
} else receivedKeyCode
|
||||||
|
|
||||||
|
val autoStart =
|
||||||
|
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
|
||||||
|
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
|
||||||
|
keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
|
||||||
|
keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS ||
|
||||||
|
keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
|
||||||
|
|
||||||
|
// We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start
|
||||||
|
onCreate(autoStart) {
|
||||||
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
|
||||||
|
KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause()
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.previous()
|
||||||
|
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.next()
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop()
|
||||||
|
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PLAY ->
|
||||||
|
if (mediaPlayerController.playerState === PlayerState.IDLE) {
|
||||||
|
mediaPlayerController.play()
|
||||||
|
} else if (mediaPlayerController.playerState !== PlayerState.STARTED) {
|
||||||
|
mediaPlayerController.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause()
|
||||||
|
KeyEvent.KEYCODE_1 -> mediaPlayerController.setSongRating(1)
|
||||||
|
KeyEvent.KEYCODE_2 -> mediaPlayerController.setSongRating(2)
|
||||||
|
KeyEvent.KEYCODE_3 -> mediaPlayerController.setSongRating(3)
|
||||||
|
KeyEvent.KEYCODE_4 -> mediaPlayerController.setSongRating(4)
|
||||||
|
KeyEvent.KEYCODE_5 -> mediaPlayerController.setSongRating(5)
|
||||||
|
KeyEvent.KEYCODE_STAR -> mediaPlayerController.toggleSongStarred()
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function processes the intent that could come from other applications.
|
||||||
|
*/
|
||||||
|
private fun handleUltrasonicIntent(intentAction: String) {
|
||||||
|
|
||||||
|
val isRunning = created
|
||||||
|
|
||||||
|
// If Ultrasonic is not running, do nothing to stop or pause
|
||||||
|
if (!isRunning && (intentAction == Constants.CMD_PAUSE ||
|
||||||
|
intentAction == Constants.CMD_STOP)) return
|
||||||
|
|
||||||
|
val autoStart =
|
||||||
|
intentAction == Constants.CMD_PLAY ||
|
||||||
|
intentAction == Constants.CMD_RESUME_OR_PLAY ||
|
||||||
|
intentAction == Constants.CMD_TOGGLEPAUSE ||
|
||||||
|
intentAction == Constants.CMD_PREVIOUS ||
|
||||||
|
intentAction == Constants.CMD_NEXT
|
||||||
|
|
||||||
|
// We can receive intents when everything is stopped, so we need to start
|
||||||
|
onCreate(autoStart) {
|
||||||
|
when (intentAction) {
|
||||||
|
Constants.CMD_PLAY -> mediaPlayerController.play()
|
||||||
|
Constants.CMD_RESUME_OR_PLAY -> // If Ultrasonic wasn't running, the autoStart is enough to resume, no need to call anything
|
||||||
|
if (isRunning) mediaPlayerController.resumeOrPlay()
|
||||||
|
|
||||||
|
Constants.CMD_NEXT -> mediaPlayerController.next()
|
||||||
|
Constants.CMD_PREVIOUS -> mediaPlayerController.previous()
|
||||||
|
Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause()
|
||||||
|
|
||||||
|
Constants.CMD_STOP -> {
|
||||||
|
// TODO: There is a stop() function, shouldn't we use that?
|
||||||
|
mediaPlayerController.pause()
|
||||||
|
mediaPlayerController.seekTo(0)
|
||||||
|
}
|
||||||
|
Constants.CMD_PAUSE -> mediaPlayerController.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,16 +8,11 @@
|
||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
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.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
@ -34,10 +29,11 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1
|
||||||
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2
|
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2
|
||||||
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3
|
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3
|
||||||
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
|
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
|
||||||
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
|
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionEventListener
|
||||||
|
import org.moire.ultrasonic.util.MediaSessionHandler
|
||||||
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
||||||
import org.moire.ultrasonic.util.ShufflePlayBuffer
|
import org.moire.ultrasonic.util.ShufflePlayBuffer
|
||||||
import org.moire.ultrasonic.util.SimpleServiceBinder
|
import org.moire.ultrasonic.util.SimpleServiceBinder
|
||||||
|
@ -59,13 +55,14 @@ class MediaPlayerService : Service() {
|
||||||
private val downloader by inject<Downloader>()
|
private val downloader by inject<Downloader>()
|
||||||
private val localMediaPlayer by inject<LocalMediaPlayer>()
|
private val localMediaPlayer by inject<LocalMediaPlayer>()
|
||||||
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
|
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
|
||||||
private val mediaPlayerLifecycleSupport by inject<MediaPlayerLifecycleSupport>()
|
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
|
||||||
private val mediaSessionEventDistributor: MediaSessionEventDistributor by inject()
|
private val mediaSessionHandler by inject<MediaSessionHandler>()
|
||||||
|
|
||||||
private var mediaSession: MediaSessionCompat? = null
|
private var mediaSession: MediaSessionCompat? = null
|
||||||
var mediaSessionToken: MediaSessionCompat.Token? = null
|
private var mediaSessionToken: MediaSessionCompat.Token? = null
|
||||||
private var isInForeground = false
|
private var isInForeground = false
|
||||||
private var notificationBuilder: NotificationCompat.Builder? = null
|
private var notificationBuilder: NotificationCompat.Builder? = null
|
||||||
|
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
||||||
|
|
||||||
private val repeatMode: RepeatMode
|
private val repeatMode: RepeatMode
|
||||||
get() = Util.getRepeatMode()
|
get() = Util.getRepeatMode()
|
||||||
|
@ -96,11 +93,18 @@ class MediaPlayerService : Service() {
|
||||||
|
|
||||||
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
|
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
|
||||||
|
|
||||||
// TODO maybe MediaSession must be in an independent class after all...
|
mediaSessionEventListener = object:MediaSessionEventListener {
|
||||||
// It seems this must be initialized in the stopped state too, e.g. for Android Auto.
|
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
|
||||||
// So it is best to init this early.
|
mediaSessionToken = token
|
||||||
initMediaSessions()
|
}
|
||||||
updateMediaSession(null, PlayerState.IDLE)
|
|
||||||
|
override fun onSkipToQueueItemRequested(id: Long) {
|
||||||
|
play(id.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
||||||
|
mediaSessionHandler.initialize()
|
||||||
|
|
||||||
// Create Notification Channel
|
// Create Notification Channel
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
|
@ -121,11 +125,13 @@ class MediaPlayerService : Service() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
instance = null
|
instance = null
|
||||||
try {
|
try {
|
||||||
|
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
||||||
|
mediaSessionHandler.release()
|
||||||
|
|
||||||
localMediaPlayer.release()
|
localMediaPlayer.release()
|
||||||
downloader.stop()
|
downloader.stop()
|
||||||
shufflePlayBuffer.onDestroy()
|
shufflePlayBuffer.onDestroy()
|
||||||
|
|
||||||
mediaSessionEventDistributor.ReleaseCachedMediaSessionToken()
|
|
||||||
mediaSession?.release()
|
mediaSession?.release()
|
||||||
mediaSession = null
|
mediaSession = null
|
||||||
} catch (ignored: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
|
@ -377,7 +383,7 @@ class MediaPlayerService : Service() {
|
||||||
val context = this@MediaPlayerService
|
val context = this@MediaPlayerService
|
||||||
|
|
||||||
// Notify MediaSession
|
// Notify MediaSession
|
||||||
updateMediaSession(currentPlaying, playerState)
|
mediaSessionHandler.updateMediaSession(currentPlaying, downloader.currentPlayingIndex.toLong(), playerState)
|
||||||
|
|
||||||
if (playerState === PlayerState.PAUSED) {
|
if (playerState === PlayerState.PAUSED) {
|
||||||
downloadQueueSerializer.serializeDownloadQueue(
|
downloadQueueSerializer.serializeDownloadQueue(
|
||||||
|
@ -477,104 +483,6 @@ class MediaPlayerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) {
|
|
||||||
Timber.d("Updating the MediaSession")
|
|
||||||
|
|
||||||
val playbackState = PlaybackStateCompat.Builder()
|
|
||||||
|
|
||||||
// Set Metadata
|
|
||||||
val metadata = MediaMetadataCompat.Builder()
|
|
||||||
if (currentPlaying != null) {
|
|
||||||
try {
|
|
||||||
val song = currentPlaying.song
|
|
||||||
val cover = BitmapUtils.getAlbumArtBitmapFromDisk(
|
|
||||||
song, Util.getMinDisplayMetric()
|
|
||||||
)
|
|
||||||
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L)
|
|
||||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist)
|
|
||||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.artist)
|
|
||||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album)
|
|
||||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title)
|
|
||||||
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover)
|
|
||||||
|
|
||||||
playbackState.setActiveQueueItemId(downloader.currentPlayingIndex.toLong())
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error setting the metadata")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the metadata
|
|
||||||
mediaSession!!.setMetadata(metadata.build())
|
|
||||||
|
|
||||||
// Create playback State
|
|
||||||
val state: Int
|
|
||||||
val isActive: Boolean
|
|
||||||
|
|
||||||
var actions: Long = PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
|
||||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
|
||||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
|
||||||
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
|
||||||
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or
|
|
||||||
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
|
||||||
|
|
||||||
// Map our playerState to native PlaybackState
|
|
||||||
// TODO: Synchronize these APIs
|
|
||||||
when (playerState) {
|
|
||||||
PlayerState.STARTED -> {
|
|
||||||
state = PlaybackStateCompat.STATE_PLAYING
|
|
||||||
isActive = true
|
|
||||||
actions = actions or
|
|
||||||
PlaybackStateCompat.ACTION_PAUSE or
|
|
||||||
PlaybackStateCompat.ACTION_STOP
|
|
||||||
}
|
|
||||||
PlayerState.COMPLETED,
|
|
||||||
PlayerState.STOPPED -> {
|
|
||||||
isActive = false
|
|
||||||
state = PlaybackStateCompat.STATE_STOPPED
|
|
||||||
}
|
|
||||||
PlayerState.IDLE -> {
|
|
||||||
isActive = false
|
|
||||||
state = PlaybackStateCompat.STATE_NONE
|
|
||||||
actions = 0L
|
|
||||||
}
|
|
||||||
PlayerState.PAUSED -> {
|
|
||||||
isActive = true
|
|
||||||
state = PlaybackStateCompat.STATE_PAUSED
|
|
||||||
actions = actions or
|
|
||||||
PlaybackStateCompat.ACTION_PLAY or
|
|
||||||
PlaybackStateCompat.ACTION_STOP
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// These are the states PREPARING, PREPARED & DOWNLOADING
|
|
||||||
isActive = true
|
|
||||||
state = PlaybackStateCompat.STATE_PAUSED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO playerPosition should be updated more frequently (currently this function is called only when the playing track changes)
|
|
||||||
playbackState.setState(state, playerPosition.toLong(), 1.0f)
|
|
||||||
|
|
||||||
// Set actions
|
|
||||||
playbackState.setActions(actions)
|
|
||||||
|
|
||||||
// Save the playback state
|
|
||||||
mediaSession!!.setPlaybackState(playbackState.build())
|
|
||||||
|
|
||||||
// Set Active state
|
|
||||||
mediaSession!!.isActive = isActive
|
|
||||||
|
|
||||||
// TODO Implement Now Playing queue handling properly
|
|
||||||
mediaSession!!.setQueueTitle("Now Playing")
|
|
||||||
mediaSession!!.setQueue(downloader.downloadList.mapIndexed { id, file ->
|
|
||||||
MediaSessionCompat.QueueItem(MediaDescriptionCompat.Builder()
|
|
||||||
.setTitle(file.song.title)
|
|
||||||
.build(), id.toLong())
|
|
||||||
})
|
|
||||||
|
|
||||||
Timber.d("Setting the MediaSession to active = %s", isActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
||||||
|
@ -814,134 +722,11 @@ class MediaPlayerService : Service() {
|
||||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initMediaSessions() {
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
val keycode = 110
|
|
||||||
|
|
||||||
Timber.w("Creating media session")
|
|
||||||
|
|
||||||
mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService")
|
|
||||||
mediaSessionToken = mediaSession!!.sessionToken
|
|
||||||
mediaSessionEventDistributor.RaiseMediaSessionTokenCreatedEvent(mediaSessionToken!!)
|
|
||||||
|
|
||||||
updateMediaButtonReceiver()
|
|
||||||
|
|
||||||
mediaSession!!.setCallback(object : MediaSessionCompat.Callback() {
|
|
||||||
override fun onPlay() {
|
|
||||||
super.onPlay()
|
|
||||||
|
|
||||||
getPendingIntentForMediaAction(
|
|
||||||
applicationContext,
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY,
|
|
||||||
keycode
|
|
||||||
).send()
|
|
||||||
|
|
||||||
Timber.v("Media Session Callback: onPlay")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
|
||||||
super.onPlayFromMediaId(mediaId, extras)
|
|
||||||
|
|
||||||
Timber.d("Media Session Callback: onPlayFromMediaId")
|
|
||||||
mediaSessionEventDistributor.RaisePlayFromMediaIdRequestedEvent(mediaId, extras)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
|
||||||
super.onPlayFromSearch(query, extras)
|
|
||||||
|
|
||||||
Timber.d("Media Session Callback: onPlayFromSearch")
|
|
||||||
mediaSessionEventDistributor.RaisePlayFromSearchRequestedEvent(query, extras)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
getPendingIntentForMediaAction(
|
|
||||||
applicationContext,
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE,
|
|
||||||
keycode
|
|
||||||
).send()
|
|
||||||
Timber.v("Media Session Callback: onPause")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
getPendingIntentForMediaAction(
|
|
||||||
applicationContext,
|
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP,
|
|
||||||
keycode
|
|
||||||
).send()
|
|
||||||
Timber.v("Media Session Callback: onStop")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSkipToNext() {
|
|
||||||
super.onSkipToNext()
|
|
||||||
getPendingIntentForMediaAction(
|
|
||||||
applicationContext,
|
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT,
|
|
||||||
keycode
|
|
||||||
).send()
|
|
||||||
Timber.v("Media Session Callback: onSkipToNext")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSkipToPrevious() {
|
|
||||||
super.onSkipToPrevious()
|
|
||||||
getPendingIntentForMediaAction(
|
|
||||||
applicationContext,
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS,
|
|
||||||
keycode
|
|
||||||
).send()
|
|
||||||
Timber.v("Media Session Callback: onSkipToPrevious")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
|
|
||||||
// This probably won't be necessary once we implement more
|
|
||||||
// of the modern media APIs, like the MediaController etc.
|
|
||||||
val event = mediaButtonEvent.extras!!["android.intent.extra.KEY_EVENT"] as KeyEvent?
|
|
||||||
mediaPlayerLifecycleSupport.handleKeyEvent(event)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSkipToQueueItem(id: Long) {
|
|
||||||
super.onSkipToQueueItem(id)
|
|
||||||
play(id.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateMediaButtonReceiver() {
|
|
||||||
if (Util.getMediaButtonsEnabled()) {
|
|
||||||
registerMediaButtonEventReceiver()
|
|
||||||
} else {
|
|
||||||
unregisterMediaButtonEventReceiver()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun registerMediaButtonEventReceiver() {
|
|
||||||
val component = ComponentName(packageName, MediaButtonIntentReceiver::class.java.name)
|
|
||||||
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
|
|
||||||
mediaButtonIntent.component = component
|
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
|
||||||
this,
|
|
||||||
INTENT_CODE_MEDIA_BUTTON,
|
|
||||||
mediaButtonIntent,
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
)
|
|
||||||
|
|
||||||
mediaSession?.setMediaButtonReceiver(pendingIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun unregisterMediaButtonEventReceiver() {
|
|
||||||
mediaSession?.setMediaButtonReceiver(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
companion object {
|
companion object {
|
||||||
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"
|
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"
|
||||||
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"
|
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"
|
||||||
private const val NOTIFICATION_ID = 3033
|
private const val NOTIFICATION_ID = 3033
|
||||||
private const val INTENT_CODE_MEDIA_BUTTON = 161
|
|
||||||
|
|
||||||
private var instance: MediaPlayerService? = null
|
private var instance: MediaPlayerService? = null
|
||||||
private val instanceLock = Any()
|
private val instanceLock = Any()
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.moire.ultrasonic.util
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.view.KeyEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class distributes MediaSession related events to its subscribers.
|
* This class distributes MediaSession related events to its subscribers.
|
||||||
|
@ -26,24 +27,32 @@ class MediaSessionEventDistributor {
|
||||||
eventListenerList.remove(listener)
|
eventListenerList.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ReleaseCachedMediaSessionToken() {
|
fun releaseCachedMediaSessionToken() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
cachedToken = null
|
cachedToken = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RaiseMediaSessionTokenCreatedEvent(token: MediaSessionCompat.Token) {
|
fun raiseMediaSessionTokenCreatedEvent(token: MediaSessionCompat.Token) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
cachedToken = token
|
cachedToken = token
|
||||||
eventListenerList.forEach { listener -> listener.onMediaSessionTokenCreated(token) }
|
eventListenerList.forEach { listener -> listener.onMediaSessionTokenCreated(token) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RaisePlayFromMediaIdRequestedEvent(mediaId: String?, extras: Bundle?) {
|
fun raisePlayFromMediaIdRequestedEvent(mediaId: String?, extras: Bundle?) {
|
||||||
eventListenerList.forEach { listener -> listener.onPlayFromMediaIdRequested(mediaId, extras) }
|
eventListenerList.forEach { listener -> listener.onPlayFromMediaIdRequested(mediaId, extras) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RaisePlayFromSearchRequestedEvent(query: String?, extras: Bundle?) {
|
fun raisePlayFromSearchRequestedEvent(query: String?, extras: Bundle?) {
|
||||||
eventListenerList.forEach { listener -> listener.onPlayFromSearchRequested(query, extras) }
|
eventListenerList.forEach { listener -> listener.onPlayFromSearchRequested(query, extras) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun raiseSkipToQueueItemRequestedEvent(id: Long) {
|
||||||
|
eventListenerList.forEach { listener -> listener.onSkipToQueueItemRequested(id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun raiseMediaButtonEvent(keyEvent: KeyEvent?) {
|
||||||
|
eventListenerList.forEach { listener -> listener.onMediaButtonEvent(keyEvent) }
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,12 +2,15 @@ package org.moire.ultrasonic.util
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.view.KeyEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback interface for MediaSession related event subscribers
|
* Callback interface for MediaSession related event subscribers
|
||||||
*/
|
*/
|
||||||
interface MediaSessionEventListener {
|
interface MediaSessionEventListener {
|
||||||
fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token)
|
fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {}
|
||||||
fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?)
|
fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {}
|
||||||
fun onPlayFromSearchRequested(query: String?, extras: Bundle?)
|
fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {}
|
||||||
|
fun onSkipToQueueItemRequested(id: Long) {}
|
||||||
|
fun onMediaButtonEvent(keyEvent: KeyEvent?) {}
|
||||||
}
|
}
|
|
@ -0,0 +1,315 @@
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
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.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
import org.moire.ultrasonic.domain.PlayerState
|
||||||
|
import org.moire.ultrasonic.imageloader.BitmapUtils
|
||||||
|
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
|
||||||
|
import org.moire.ultrasonic.service.DownloadFile
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private const val INTENT_CODE_MEDIA_BUTTON = 161
|
||||||
|
|
||||||
|
class MediaSessionHandler : KoinComponent {
|
||||||
|
|
||||||
|
private var mediaSession: MediaSessionCompat? = null
|
||||||
|
private var playbackState: Int? = null
|
||||||
|
private var playbackActions: Long? = null
|
||||||
|
private var cachedPlayingIndex: Long? = null
|
||||||
|
|
||||||
|
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
|
||||||
|
private val applicationContext by inject<Context>()
|
||||||
|
|
||||||
|
private var referenceCount: Int = 0
|
||||||
|
private var cachedPlaylist: Iterable<MusicDirectory.Entry>? = null
|
||||||
|
private var playbackPositionDelayCount: Int = 0
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
|
||||||
|
if (referenceCount > 0) referenceCount--
|
||||||
|
if (referenceCount > 0) return
|
||||||
|
|
||||||
|
mediaSession?.isActive = false
|
||||||
|
mediaSessionEventDistributor.releaseCachedMediaSessionToken()
|
||||||
|
mediaSession?.release()
|
||||||
|
mediaSession = null
|
||||||
|
|
||||||
|
Timber.i("MediaSessionHandler.initialize Media Session released")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initialize() {
|
||||||
|
|
||||||
|
referenceCount++
|
||||||
|
if (referenceCount > 1) return
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
val keycode = 110
|
||||||
|
|
||||||
|
Timber.d("MediaSessionHandler.initialize Creating Media Session")
|
||||||
|
|
||||||
|
mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService")
|
||||||
|
val mediaSessionToken = mediaSession!!.sessionToken
|
||||||
|
mediaSessionEventDistributor.raiseMediaSessionTokenCreatedEvent(mediaSessionToken!!)
|
||||||
|
|
||||||
|
updateMediaButtonReceiver()
|
||||||
|
|
||||||
|
mediaSession!!.setCallback(object : MediaSessionCompat.Callback() {
|
||||||
|
override fun onPlay() {
|
||||||
|
super.onPlay()
|
||||||
|
|
||||||
|
getPendingIntentForMediaAction(
|
||||||
|
applicationContext,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PLAY,
|
||||||
|
keycode
|
||||||
|
).send()
|
||||||
|
|
||||||
|
Timber.v("Media Session Callback: onPlay")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
|
super.onPlayFromMediaId(mediaId, extras)
|
||||||
|
|
||||||
|
Timber.d("Media Session Callback: onPlayFromMediaId")
|
||||||
|
mediaSessionEventDistributor.raisePlayFromMediaIdRequestedEvent(mediaId, extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
|
super.onPlayFromSearch(query, extras)
|
||||||
|
|
||||||
|
Timber.d("Media Session Callback: onPlayFromSearch")
|
||||||
|
mediaSessionEventDistributor.raisePlayFromSearchRequestedEvent(query, extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
getPendingIntentForMediaAction(
|
||||||
|
applicationContext,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PAUSE,
|
||||||
|
keycode
|
||||||
|
).send()
|
||||||
|
Timber.v("Media Session Callback: onPause")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
getPendingIntentForMediaAction(
|
||||||
|
applicationContext,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP,
|
||||||
|
keycode
|
||||||
|
).send()
|
||||||
|
Timber.v("Media Session Callback: onStop")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSkipToNext() {
|
||||||
|
super.onSkipToNext()
|
||||||
|
getPendingIntentForMediaAction(
|
||||||
|
applicationContext,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_NEXT,
|
||||||
|
keycode
|
||||||
|
).send()
|
||||||
|
Timber.v("Media Session Callback: onSkipToNext")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSkipToPrevious() {
|
||||||
|
super.onSkipToPrevious()
|
||||||
|
getPendingIntentForMediaAction(
|
||||||
|
applicationContext,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS,
|
||||||
|
keycode
|
||||||
|
).send()
|
||||||
|
Timber.v("Media Session Callback: onSkipToPrevious")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
|
||||||
|
// This probably won't be necessary once we implement more
|
||||||
|
// of the modern media APIs, like the MediaController etc.
|
||||||
|
val event = mediaButtonEvent.extras!!["android.intent.extra.KEY_EVENT"] as KeyEvent?
|
||||||
|
mediaSessionEventDistributor.raiseMediaButtonEvent(event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSkipToQueueItem(id: Long) {
|
||||||
|
super.onSkipToQueueItem(id)
|
||||||
|
mediaSessionEventDistributor.raiseSkipToQueueItemRequestedEvent(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// It seems to be the best practice to set this to true for the lifetime of the session
|
||||||
|
mediaSession!!.isActive = true
|
||||||
|
if (cachedPlaylist != null) updateMediaSessionQueue(cachedPlaylist!!)
|
||||||
|
Timber.i("MediaSessionHandler.initialize Media Session created")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMediaSession(currentPlaying: DownloadFile?, currentPlayingIndex: Long?, playerState: PlayerState) {
|
||||||
|
Timber.d("Updating the MediaSession")
|
||||||
|
|
||||||
|
// Set Metadata
|
||||||
|
val metadata = MediaMetadataCompat.Builder()
|
||||||
|
if (currentPlaying != null) {
|
||||||
|
try {
|
||||||
|
val song = currentPlaying.song
|
||||||
|
val cover = BitmapUtils.getAlbumArtBitmapFromDisk(
|
||||||
|
song, Util.getMinDisplayMetric()
|
||||||
|
)
|
||||||
|
val duration = song.duration?.times(1000) ?: -1
|
||||||
|
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration.toLong())
|
||||||
|
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist)
|
||||||
|
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.artist)
|
||||||
|
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album)
|
||||||
|
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title)
|
||||||
|
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Error setting the metadata")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the metadata
|
||||||
|
mediaSession!!.setMetadata(metadata.build())
|
||||||
|
|
||||||
|
playbackActions = PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||||
|
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||||
|
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||||
|
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
||||||
|
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or
|
||||||
|
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||||
|
|
||||||
|
// Map our playerState to native PlaybackState
|
||||||
|
// TODO: Synchronize these APIs
|
||||||
|
when (playerState) {
|
||||||
|
PlayerState.STARTED -> {
|
||||||
|
playbackState = PlaybackStateCompat.STATE_PLAYING
|
||||||
|
playbackActions = playbackActions!! or
|
||||||
|
PlaybackStateCompat.ACTION_PAUSE or
|
||||||
|
PlaybackStateCompat.ACTION_STOP
|
||||||
|
}
|
||||||
|
PlayerState.COMPLETED,
|
||||||
|
PlayerState.STOPPED -> {
|
||||||
|
playbackState = PlaybackStateCompat.STATE_STOPPED
|
||||||
|
}
|
||||||
|
PlayerState.IDLE -> {
|
||||||
|
// IDLE state usually just means the playback is stopped
|
||||||
|
// STATE_NONE means that there is no track to play (playlist is empty)
|
||||||
|
playbackState = if (currentPlaying == null)
|
||||||
|
PlaybackStateCompat.STATE_NONE
|
||||||
|
else
|
||||||
|
PlaybackStateCompat.STATE_STOPPED
|
||||||
|
playbackActions = 0L
|
||||||
|
}
|
||||||
|
PlayerState.PAUSED -> {
|
||||||
|
playbackState = PlaybackStateCompat.STATE_PAUSED
|
||||||
|
playbackActions = playbackActions!! or
|
||||||
|
PlaybackStateCompat.ACTION_PLAY or
|
||||||
|
PlaybackStateCompat.ACTION_STOP
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// These are the states PREPARING, PREPARED & DOWNLOADING
|
||||||
|
playbackState = PlaybackStateCompat.STATE_PAUSED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val playbackStateBuilder = PlaybackStateCompat.Builder()
|
||||||
|
playbackStateBuilder.setState(playbackState!!, PLAYBACK_POSITION_UNKNOWN, 1.0f)
|
||||||
|
|
||||||
|
// Set actions
|
||||||
|
playbackStateBuilder.setActions(playbackActions!!)
|
||||||
|
|
||||||
|
cachedPlayingIndex = currentPlayingIndex
|
||||||
|
if (currentPlayingIndex != null)
|
||||||
|
playbackStateBuilder.setActiveQueueItemId(currentPlayingIndex)
|
||||||
|
|
||||||
|
// Save the playback state
|
||||||
|
mediaSession!!.setPlaybackState(playbackStateBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMediaSessionQueue(playlist: Iterable<MusicDirectory.Entry>)
|
||||||
|
{
|
||||||
|
// This call is cached because Downloader may initialize earlier than the MediaSession
|
||||||
|
cachedPlaylist = playlist
|
||||||
|
if (mediaSession == null) return
|
||||||
|
|
||||||
|
// TODO Implement Now Playing queue handling properly
|
||||||
|
mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
|
||||||
|
mediaSession!!.setQueue(playlist.mapIndexed { id, song ->
|
||||||
|
MediaSessionCompat.QueueItem(
|
||||||
|
MediaDescriptionCompat.Builder()
|
||||||
|
.setTitle(song.title)
|
||||||
|
.build(), id.toLong())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMediaSessionPlaybackPosition(playbackPosition: Long) {
|
||||||
|
|
||||||
|
if (mediaSession == null) return
|
||||||
|
|
||||||
|
if (playbackState == null || playbackActions == null) return
|
||||||
|
|
||||||
|
// Playback position is updated too frequently in the player.
|
||||||
|
// This counter makes sure that the MediaSession is updated ~ at every second
|
||||||
|
playbackPositionDelayCount++
|
||||||
|
if (playbackPositionDelayCount < 10) return
|
||||||
|
|
||||||
|
playbackPositionDelayCount = 0
|
||||||
|
val playbackStateBuilder = PlaybackStateCompat.Builder()
|
||||||
|
playbackStateBuilder.setState(playbackState!!, playbackPosition, 1.0f)
|
||||||
|
playbackStateBuilder.setActions(playbackActions!!)
|
||||||
|
|
||||||
|
if (cachedPlayingIndex != null)
|
||||||
|
playbackStateBuilder.setActiveQueueItemId(cachedPlayingIndex!!)
|
||||||
|
|
||||||
|
mediaSession!!.setPlaybackState(playbackStateBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMediaButtonReceiver() {
|
||||||
|
if (Util.getMediaButtonsEnabled()) {
|
||||||
|
registerMediaButtonEventReceiver()
|
||||||
|
} else {
|
||||||
|
unregisterMediaButtonEventReceiver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerMediaButtonEventReceiver() {
|
||||||
|
val component = ComponentName(applicationContext.packageName, MediaButtonIntentReceiver::class.java.name)
|
||||||
|
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
|
||||||
|
mediaButtonIntent.component = component
|
||||||
|
|
||||||
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
applicationContext,
|
||||||
|
INTENT_CODE_MEDIA_BUTTON,
|
||||||
|
mediaButtonIntent,
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
mediaSession?.setMediaButtonReceiver(pendingIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterMediaButtonEventReceiver() {
|
||||||
|
mediaSession?.setMediaButtonReceiver(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Copied from MediaPlayerService. Move to Utils
|
||||||
|
private fun getPendingIntentForMediaAction(
|
||||||
|
context: Context,
|
||||||
|
keycode: Int,
|
||||||
|
requestCode: Int
|
||||||
|
): PendingIntent {
|
||||||
|
val intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||||
|
val flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
intent.setPackage(context.packageName)
|
||||||
|
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
||||||
|
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue