mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-18 04:30:48 +01:00
Merge branch 'develop' into video-cleanup
This commit is contained in:
commit
2655a4a606
@ -10,7 +10,7 @@ ext.versions = [
|
|||||||
androidxcore : "1.5.0",
|
androidxcore : "1.5.0",
|
||||||
ktlint : "0.37.1",
|
ktlint : "0.37.1",
|
||||||
ktlintGradle : "9.2.1",
|
ktlintGradle : "9.2.1",
|
||||||
detekt : "1.17.1",
|
detekt : "1.18.0",
|
||||||
jacoco : "0.8.7",
|
jacoco : "0.8.7",
|
||||||
preferences : "1.1.1",
|
preferences : "1.1.1",
|
||||||
media : "1.3.1",
|
media : "1.3.1",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.moire.ultrasonic"
|
package="org.moire.ultrasonic"
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
|
|
||||||
@ -27,6 +28,14 @@
|
|||||||
android:name=".app.UApp"
|
android:name=".app.UApp"
|
||||||
android:label="@string/common.appname"
|
android:label="@string/common.appname"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
|
<meta-data android:name="com.google.android.gms.car.application"
|
||||||
|
android:resource="@xml/automotive_app_desc"/>
|
||||||
|
|
||||||
|
<!--Used by Android Auto-->
|
||||||
|
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||||
|
android:resource="@mipmap/ic_launcher" />
|
||||||
|
|
||||||
<activity android:name=".activity.NavigationActivity"
|
<activity android:name=".activity.NavigationActivity"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
android:label="@string/common.appname"
|
android:label="@string/common.appname"
|
||||||
@ -51,6 +60,17 @@
|
|||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
tools:ignore="ExportedService"
|
||||||
|
android:name=".service.AutoMediaBrowserService"
|
||||||
|
android:label="@string/common.appname"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver android:name=".receiver.MediaButtonIntentReceiver">
|
<receiver android:name=".receiver.MediaButtonIntentReceiver">
|
||||||
<intent-filter android:priority="2147483647">
|
<intent-filter android:priority="2147483647">
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -125,8 +125,6 @@ public final class Constants
|
|||||||
public static final String PREFERENCES_KEY_DEFAULT_SHARE_GREETING = "sharingDefaultGreeting";
|
public static final String PREFERENCES_KEY_DEFAULT_SHARE_GREETING = "sharingDefaultGreeting";
|
||||||
public static final String PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION = "sharingDefaultExpiration";
|
public static final String PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION = "sharingDefaultExpiration";
|
||||||
public static final String PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist";
|
public static final String PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist";
|
||||||
public static final String PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY = "imageLoaderConcurrency";
|
|
||||||
public static final String PREFERENCES_KEY_FF_IMAGE_LOADER = "ff_new_image_loader";
|
|
||||||
public static final String PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating";
|
public static final String PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating";
|
||||||
public static final String PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory";
|
public static final String PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory";
|
||||||
public static final String PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted";
|
public static final String PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted";
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,8 @@ import org.koin.android.ext.koin.androidContext
|
|||||||
import org.koin.dsl.module
|
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.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
|
||||||
@ -17,4 +19,6 @@ val applicationModule = module {
|
|||||||
single { PermissionUtil(androidContext()) }
|
single { PermissionUtil(androidContext()) }
|
||||||
single { NowPlayingEventDistributor() }
|
single { NowPlayingEventDistributor() }
|
||||||
single { ThemeChangedEventDistributor() }
|
single { ThemeChangedEventDistributor() }
|
||||||
|
single { MediaSessionEventDistributor() }
|
||||||
|
single { MediaSessionHandler() }
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.moire.ultrasonic.di
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.service.AudioFocusHandler
|
import org.moire.ultrasonic.service.AudioFocusHandler
|
||||||
import org.moire.ultrasonic.service.DownloadQueueSerializer
|
import org.moire.ultrasonic.service.DownloadQueueSerializer
|
||||||
@ -17,12 +16,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.
|
||||||
|
@ -313,7 +313,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
|||||||
}
|
}
|
||||||
|
|
||||||
repeatButton.setOnClickListener {
|
repeatButton.setOnClickListener {
|
||||||
val repeatMode = mediaPlayerController.repeatMode?.next()
|
val repeatMode = mediaPlayerController.repeatMode.next()
|
||||||
mediaPlayerController.repeatMode = repeatMode
|
mediaPlayerController.repeatMode = repeatMode
|
||||||
onDownloadListChanged()
|
onDownloadListChanged()
|
||||||
when (repeatMode) {
|
when (repeatMode) {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* DownloadQueueSerializer.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.locks.Lock
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
@ -26,12 +26,15 @@ import java.net.URLEncoder
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||||
import org.moire.ultrasonic.audiofx.VisualizerController
|
import org.moire.ultrasonic.audiofx.VisualizerController
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
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,12 @@ 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(
|
@Suppress("TooManyFunctions")
|
||||||
private val audioFocusHandler: AudioFocusHandler,
|
class LocalMediaPlayer : KoinComponent {
|
||||||
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
|
||||||
@ -125,6 +130,10 @@ class LocalMediaPlayer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
|
// Calling reset() will result in changing this player's state. If we allow
|
||||||
|
// the onPlayerStateChanged callback, then the state change will cause this
|
||||||
|
// to resurrect the media session which has just been destroyed.
|
||||||
|
onPlayerStateChanged = null
|
||||||
reset()
|
reset()
|
||||||
try {
|
try {
|
||||||
val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
||||||
@ -167,7 +176,7 @@ class LocalMediaPlayer(
|
|||||||
val mainHandler = Handler(context.mainLooper)
|
val mainHandler = Handler(context.mainLooper)
|
||||||
|
|
||||||
val myRunnable = Runnable {
|
val myRunnable = Runnable {
|
||||||
onPlayerStateChanged!!(playerState, currentPlaying)
|
onPlayerStateChanged?.invoke(playerState, currentPlaying)
|
||||||
}
|
}
|
||||||
mainHandler.post(myRunnable)
|
mainHandler.post(myRunnable)
|
||||||
}
|
}
|
||||||
@ -701,8 +710,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
|
||||||
|
@ -247,10 +247,10 @@ class MediaPlayerController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@set:Synchronized
|
@set:Synchronized
|
||||||
var repeatMode: RepeatMode?
|
var repeatMode: RepeatMode
|
||||||
get() = Util.getRepeatMode()
|
get() = Util.repeatMode
|
||||||
set(repeatMode) {
|
set(repeatMode) {
|
||||||
Util.setRepeatMode(repeatMode)
|
Util.repeatMode = repeatMode
|
||||||
val mediaPlayerService = runningInstance
|
val mediaPlayerService = runningInstance
|
||||||
mediaPlayerService?.setNextPlaying()
|
mediaPlayerService?.setNextPlaying()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
* MediaPlayerLifecycleSupport.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber", "ComplexMethod")
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,17 +12,15 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
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.IBinder
|
import android.os.IBinder
|
||||||
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
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.activity.NavigationActivity
|
import org.moire.ultrasonic.activity.NavigationActivity
|
||||||
@ -35,9 +33,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.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,15 +59,17 @@ 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 mediaSessionHandler by inject<MediaSessionHandler>()
|
||||||
|
|
||||||
private var mediaSession: MediaSessionCompat? = null
|
private var mediaSession: MediaSessionCompat? = null
|
||||||
private 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.repeatMode
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
return binder
|
return binder
|
||||||
@ -95,6 +97,19 @@ class MediaPlayerService : Service() {
|
|||||||
|
|
||||||
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
|
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
|
||||||
|
|
||||||
|
mediaSessionEventListener = object : MediaSessionEventListener {
|
||||||
|
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
|
||||||
|
mediaSessionToken = token
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSkipToQueueItemRequested(id: Long) {
|
||||||
|
play(id.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
||||||
|
mediaSessionHandler.initialize()
|
||||||
|
|
||||||
// Create Notification Channel
|
// Create Notification Channel
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
|
|
||||||
@ -114,9 +129,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()
|
||||||
|
|
||||||
mediaSession?.release()
|
mediaSession?.release()
|
||||||
mediaSession = null
|
mediaSession = null
|
||||||
} catch (ignored: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
@ -368,7 +387,11 @@ 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(
|
||||||
@ -468,90 +491,6 @@ class MediaPlayerService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) {
|
|
||||||
Timber.d("Updating the MediaSession")
|
|
||||||
|
|
||||||
if (mediaSession == null) initMediaSessions()
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error setting the metadata")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the metadata
|
|
||||||
mediaSession!!.setMetadata(metadata.build())
|
|
||||||
|
|
||||||
// Create playback State
|
|
||||||
val playbackState = PlaybackStateCompat.Builder()
|
|
||||||
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
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f)
|
|
||||||
|
|
||||||
// Set actions
|
|
||||||
playbackState.setActions(actions)
|
|
||||||
|
|
||||||
// Save the playback state
|
|
||||||
mediaSession!!.setPlaybackState(playbackState.build())
|
|
||||||
|
|
||||||
// Set Active state
|
|
||||||
mediaSession!!.isActive = isActive
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|
||||||
@ -604,7 +543,11 @@ class MediaPlayerService : Service() {
|
|||||||
// Init
|
// Init
|
||||||
val context = applicationContext
|
val context = applicationContext
|
||||||
val song = currentPlaying?.song
|
val song = currentPlaying?.song
|
||||||
val stopIntent = getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_STOP, 100)
|
val stopIntent = Util.getPendingIntentForMediaAction(
|
||||||
|
context,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP,
|
||||||
|
100
|
||||||
|
)
|
||||||
|
|
||||||
// We should use a single notification builder, otherwise the notification may not be updated
|
// We should use a single notification builder, otherwise the notification may not be updated
|
||||||
if (notificationBuilder == null) {
|
if (notificationBuilder == null) {
|
||||||
@ -723,7 +666,7 @@ class MediaPlayerService : Service() {
|
|||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode)
|
val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode)
|
||||||
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
|
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -734,7 +677,7 @@ class MediaPlayerService : Service() {
|
|||||||
): NotificationCompat.Action {
|
): NotificationCompat.Action {
|
||||||
val isPlaying = playerState === PlayerState.STARTED
|
val isPlaying = playerState === PlayerState.STARTED
|
||||||
val keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
val keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||||
val pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode)
|
val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode)
|
||||||
val label: String
|
val label: String
|
||||||
val icon: Int
|
val icon: Int
|
||||||
|
|
||||||
@ -767,7 +710,7 @@ class MediaPlayerService : Service() {
|
|||||||
icon = R.drawable.ic_star_hollow_dark
|
icon = R.drawable.ic_star_hollow_dark
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent = getPendingIntentForMediaAction(context, keyCode, requestCode)
|
val pendingIntent = Util.getPendingIntentForMediaAction(context, keyCode, requestCode)
|
||||||
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
|
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -779,126 +722,11 @@ class MediaPlayerService : Service() {
|
|||||||
return PendingIntent.getActivity(this, 0, intent, flags)
|
return PendingIntent.getActivity(this, 0, intent, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initMediaSessions() {
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
val keycode = 110
|
|
||||||
|
|
||||||
Timber.w("Creating media session")
|
|
||||||
|
|
||||||
mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService")
|
|
||||||
mediaSessionToken = mediaSession!!.sessionToken
|
|
||||||
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
||||||
|
@ -40,13 +40,13 @@ class ShareHandler(val context: Context) {
|
|||||||
swipe: SwipeRefreshLayout?,
|
swipe: SwipeRefreshLayout?,
|
||||||
cancellationToken: CancellationToken
|
cancellationToken: CancellationToken
|
||||||
) {
|
) {
|
||||||
val askForDetails = Util.getShouldAskForShareDetails()
|
val askForDetails = Util.shouldAskForShareDetails
|
||||||
val shareDetails = ShareDetails()
|
val shareDetails = ShareDetails()
|
||||||
shareDetails.Entries = entries
|
shareDetails.Entries = entries
|
||||||
if (askForDetails) {
|
if (askForDetails) {
|
||||||
showDialog(fragment, shareDetails, swipe, cancellationToken)
|
showDialog(fragment, shareDetails, swipe, cancellationToken)
|
||||||
} else {
|
} else {
|
||||||
shareDetails.Description = Util.getDefaultShareDescription()
|
shareDetails.Description = Util.defaultShareDescription
|
||||||
shareDetails.Expiration = TimeSpan.getCurrentTime().add(
|
shareDetails.Expiration = TimeSpan.getCurrentTime().add(
|
||||||
Util.getDefaultShareExpirationInMillis(context)
|
Util.getDefaultShareExpirationInMillis(context)
|
||||||
).totalMilliseconds
|
).totalMilliseconds
|
||||||
@ -133,16 +133,16 @@ class ShareHandler(val context: Context) {
|
|||||||
}
|
}
|
||||||
shareDetails.Description = shareDescription!!.text.toString()
|
shareDetails.Description = shareDescription!!.text.toString()
|
||||||
if (hideDialogCheckBox!!.isChecked) {
|
if (hideDialogCheckBox!!.isChecked) {
|
||||||
Util.setShouldAskForShareDetails(false)
|
Util.shouldAskForShareDetails = false
|
||||||
}
|
}
|
||||||
if (saveAsDefaultsCheckBox!!.isChecked) {
|
if (saveAsDefaultsCheckBox!!.isChecked) {
|
||||||
val timeSpanType: String = timeSpanPicker!!.timeSpanType
|
val timeSpanType: String = timeSpanPicker!!.timeSpanType
|
||||||
val timeSpanAmount: Int = timeSpanPicker!!.timeSpanAmount
|
val timeSpanAmount: Int = timeSpanPicker!!.timeSpanAmount
|
||||||
Util.setDefaultShareExpiration(
|
Util.defaultShareExpiration =
|
||||||
if (!noExpirationCheckBox!!.isChecked && timeSpanAmount > 0)
|
if (!noExpirationCheckBox!!.isChecked && timeSpanAmount > 0)
|
||||||
String.format("%d:%s", timeSpanAmount, timeSpanType) else ""
|
String.format("%d:%s", timeSpanAmount, timeSpanType) else ""
|
||||||
)
|
|
||||||
Util.setDefaultShareDescription(shareDetails.Description)
|
Util.defaultShareDescription = shareDetails.Description
|
||||||
}
|
}
|
||||||
share(fragment, shareDetails, swipe, cancellationToken)
|
share(fragment, shareDetails, swipe, cancellationToken)
|
||||||
}
|
}
|
||||||
@ -157,8 +157,8 @@ class ShareHandler(val context: Context) {
|
|||||||
b ->
|
b ->
|
||||||
timeSpanPicker!!.isEnabled = !b
|
timeSpanPicker!!.isEnabled = !b
|
||||||
}
|
}
|
||||||
val defaultDescription = Util.getDefaultShareDescription()
|
val defaultDescription = Util.defaultShareDescription
|
||||||
val timeSpan = Util.getDefaultShareExpiration()
|
val timeSpan = Util.defaultShareExpiration
|
||||||
val split = pattern.split(timeSpan)
|
val split = pattern.split(timeSpan)
|
||||||
if (split.size == 2) {
|
if (split.size == 2) {
|
||||||
val timeSpanAmount = split[0].toInt()
|
val timeSpanAmount = split[0].toInt()
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* MediaSessionEventDistributor.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.view.KeyEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class distributes MediaSession related events to its subscribers.
|
||||||
|
* It is a primitive implementation of a pub-sub event bus
|
||||||
|
*/
|
||||||
|
class MediaSessionEventDistributor {
|
||||||
|
var eventListenerList: MutableList<MediaSessionEventListener> =
|
||||||
|
listOf<MediaSessionEventListener>().toMutableList()
|
||||||
|
|
||||||
|
var cachedToken: MediaSessionCompat.Token? = null
|
||||||
|
|
||||||
|
fun subscribe(listener: MediaSessionEventListener) {
|
||||||
|
eventListenerList.add(listener)
|
||||||
|
|
||||||
|
synchronized(this) {
|
||||||
|
if (cachedToken != null)
|
||||||
|
listener.onMediaSessionTokenCreated(cachedToken!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unsubscribe(listener: MediaSessionEventListener) {
|
||||||
|
eventListenerList.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun releaseCachedMediaSessionToken() {
|
||||||
|
synchronized(this) {
|
||||||
|
cachedToken = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun raiseMediaSessionTokenCreatedEvent(token: MediaSessionCompat.Token) {
|
||||||
|
synchronized(this) {
|
||||||
|
cachedToken = token
|
||||||
|
eventListenerList.forEach { listener -> listener.onMediaSessionTokenCreated(token) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun raisePlayFromMediaIdRequestedEvent(mediaId: String?, extras: Bundle?) {
|
||||||
|
eventListenerList.forEach {
|
||||||
|
listener ->
|
||||||
|
listener.onPlayFromMediaIdRequested(mediaId, extras)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun raisePlayFromSearchRequestedEvent(query: String?, extras: Bundle?) {
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* MediaSessionEventListener.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.view.KeyEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface for MediaSession related event subscribers
|
||||||
|
*/
|
||||||
|
interface MediaSessionEventListener {
|
||||||
|
fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {}
|
||||||
|
fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {}
|
||||||
|
fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {}
|
||||||
|
fun onSkipToQueueItemRequested(id: Long) {}
|
||||||
|
fun onMediaButtonEvent(keyEvent: KeyEvent?) {}
|
||||||
|
}
|
@ -0,0 +1,323 @@
|
|||||||
|
/*
|
||||||
|
* MediaSessionHandler.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.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
|
||||||
|
private const val CALL_DIVIDE = 10
|
||||||
|
/**
|
||||||
|
* Central place to handle the state of the MediaSession
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
private var cachedPosition: Long = 0
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
|
||||||
|
if (referenceCount > 0) referenceCount--
|
||||||
|
if (referenceCount > 0) return
|
||||||
|
|
||||||
|
mediaSession?.isActive = false
|
||||||
|
mediaSessionEventDistributor.releaseCachedMediaSessionToken()
|
||||||
|
mediaSession?.release()
|
||||||
|
mediaSession = null
|
||||||
|
|
||||||
|
Timber.i("MediaSessionHandler.release 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()
|
||||||
|
|
||||||
|
Util.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 %s", mediaId)
|
||||||
|
mediaSessionEventDistributor.raisePlayFromMediaIdRequestedEvent(mediaId, extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
|
super.onPlayFromSearch(query, extras)
|
||||||
|
|
||||||
|
Timber.d("Media Session Callback: onPlayFromSearch %s", query)
|
||||||
|
mediaSessionEventDistributor.raisePlayFromSearchRequestedEvent(query, extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
Util.getPendingIntentForMediaAction(
|
||||||
|
applicationContext,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PAUSE,
|
||||||
|
keycode
|
||||||
|
).send()
|
||||||
|
Timber.v("Media Session Callback: onPause")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
Util.getPendingIntentForMediaAction(
|
||||||
|
applicationContext,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP,
|
||||||
|
keycode
|
||||||
|
).send()
|
||||||
|
Timber.v("Media Session Callback: onStop")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSkipToNext() {
|
||||||
|
super.onSkipToNext()
|
||||||
|
Util.getPendingIntentForMediaAction(
|
||||||
|
applicationContext,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_NEXT,
|
||||||
|
keycode
|
||||||
|
).send()
|
||||||
|
Timber.v("Media Session Callback: onSkipToNext")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSkipToPrevious() {
|
||||||
|
super.onSkipToPrevious()
|
||||||
|
Util.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")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionCaught", "LongMethod")
|
||||||
|
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
|
||||||
|
cachedPosition = PLAYBACK_POSITION_UNKNOWN
|
||||||
|
}
|
||||||
|
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
|
||||||
|
cachedPosition = PLAYBACK_POSITION_UNKNOWN
|
||||||
|
}
|
||||||
|
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!!, cachedPosition, 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
|
||||||
|
|
||||||
|
mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
|
||||||
|
mediaSession!!.setQueue(
|
||||||
|
playlist.mapIndexed { id, song ->
|
||||||
|
MediaSessionCompat.QueueItem(
|
||||||
|
Util.getMediaDescriptionForEntry(song),
|
||||||
|
id.toLong()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMediaSessionPlaybackPosition(playbackPosition: Long) {
|
||||||
|
|
||||||
|
cachedPosition = playbackPosition
|
||||||
|
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 < CALL_DIVIDE) 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)
|
||||||
|
}
|
||||||
|
}
|
1351
ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
Normal file
1351
ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
Normal file
File diff suppressed because it is too large
Load Diff
9
ultrasonic/src/main/res/drawable/ic_artist.xml
Normal file
9
ultrasonic/src/main/res/drawable/ic_artist.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:pathData="M9,13.75c-2.34,0 -7,1.17 -7,3.5L2,19h14v-1.75c0,-2.33 -4.66,-3.5 -7,-3.5zM4.34,17c0.84,-0.58 2.87,-1.25 4.66,-1.25s3.82,0.67 4.66,1.25L4.34,17zM9,12c1.93,0 3.5,-1.57 3.5,-3.5S10.93,5 9,5 5.5,6.57 5.5,8.5 7.07,12 9,12zM9,7c0.83,0 1.5,0.67 1.5,1.5S9.83,10 9,10s-1.5,-0.67 -1.5,-1.5S8.17,7 9,7zM16.04,13.81c1.16,0.84 1.96,1.96 1.96,3.44L18,19h4v-1.75c0,-2.02 -3.5,-3.17 -5.96,-3.44zM15,12c1.93,0 3.5,-1.57 3.5,-3.5S16.93,5 15,5c-0.54,0 -1.04,0.13 -1.5,0.35 0.63,0.89 1,1.98 1,3.15s-0.37,2.26 -1,3.15c0.46,0.22 0.96,0.35 1.5,0.35z"/>
|
||||||
|
</vector>
|
9
ultrasonic/src/main/res/drawable/ic_library.xml
Normal file
9
ultrasonic/src/main/res/drawable/ic_library.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:pathData="M22,6h-5v8.18C16.69,14.07 16.35,14 16,14c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3V8h3V6zM15,6H3v2h12V6zM15,10H3v2h12V10zM11,14H3v2h8V14z"/>
|
||||||
|
</vector>
|
3
ultrasonic/src/main/res/xml/automotive_app_desc.xml
Normal file
3
ultrasonic/src/main/res/xml/automotive_app_desc.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<automotiveApp>
|
||||||
|
<uses name="media"/>
|
||||||
|
</automotiveApp>
|
Loading…
x
Reference in New Issue
Block a user