From 53628dde54c076f7a6269f1e10071b5bc61440b5 Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 22 Jun 2020 18:35:58 +0200 Subject: [PATCH] Started to use Koin, refactored lifecycleSupport and Intent handling --- .../activity/EqualizerActivity.java | 15 +- .../ultrasonic/activity/MainActivity.java | 9 +- .../activity/SelectPlaylistActivity.java | 2 +- .../activity/SubsonicTabActivity.java | 48 +-- .../ultrasonic/fragment/SettingsFragment.java | 9 +- .../provider/UltraSonicAppWidgetProvider.java | 23 +- .../receiver/A2dpIntentReceiver.java | 24 +- .../receiver/BluetoothIntentReceiver.java | 4 +- .../receiver/MediaButtonIntentReceiver.java | 23 +- .../moire/ultrasonic/service/Consumer.java | 6 + .../ultrasonic/service/DownloadFile.java | 4 +- .../service/DownloadQueueSerializer.java | 105 ++++++ .../service/DownloadServiceImpl.java | 313 +++++++++-------- .../DownloadServiceLifecycleSupport.java | 329 ++++-------------- .../service/ExternalStorageMonitor.java | 55 +++ .../ultrasonic/service/JukeboxService.java | 32 +- .../service/MediaPlayerService.java | 94 +++-- .../service/OfflineMusicService.java | 6 - .../org/moire/ultrasonic/service/State.java | 16 + .../moire/ultrasonic/util/CacheCleaner.java | 24 +- .../org/moire/ultrasonic/util/Constants.java | 11 + .../java/org/moire/ultrasonic/util/Util.java | 55 +-- .../org/moire/ultrasonic/view/SongView.java | 25 +- .../moire/ultrasonic/view/VisualizerView.java | 15 +- .../moire/ultrasonic/di/MusicServiceModule.kt | 12 +- 25 files changed, 651 insertions(+), 608 deletions(-) create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadQueueSerializer.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/State.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java index 58047244..329bdf79 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java @@ -38,6 +38,10 @@ import org.moire.ultrasonic.service.DownloadServiceImpl; import java.util.HashMap; import java.util.Map; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * Equalizer controls. * @@ -52,6 +56,8 @@ public class EqualizerActivity extends ResultActivity private EqualizerController equalizerController; private Equalizer equalizer; + private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + @Override public void onCreate(Bundle bundle) { @@ -123,14 +129,7 @@ public class EqualizerActivity extends ResultActivity private void setup() { - DownloadService instance = DownloadServiceImpl.getInstance(); - - if (instance == null) - { - return; - } - - equalizerController = instance.getEqualizerController(); + equalizerController = downloadServiceImpl.getValue().getEqualizerController(); equalizer = equalizerController.getEqualizer(); initEqualizer(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java index c5a4c53a..1b053e19 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java @@ -36,6 +36,8 @@ import android.widget.TextView; import org.moire.ultrasonic.R; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.DownloadServiceLifecycleSupport; +import org.moire.ultrasonic.service.ExternalStorageMonitor; import org.moire.ultrasonic.service.MediaPlayerService; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; @@ -47,7 +49,10 @@ import org.moire.ultrasonic.util.Util; import java.util.Collections; +import kotlin.Lazy; + import static java.util.Arrays.asList; +import static org.koin.java.standalone.KoinJavaComponent.inject; public class MainActivity extends SubsonicTabActivity { @@ -68,6 +73,8 @@ public class MainActivity extends SubsonicTabActivity private static boolean infoDialogDisplayed; private static boolean shouldUseId3; + private Lazy lifecycleSupport = inject(DownloadServiceLifecycleSupport.class); + /** * Called when the activity is first created. */ @@ -477,7 +484,7 @@ public class MainActivity extends SubsonicTabActivity private void exit() { - DownloadServiceImpl.getInstance().onCommand(new Intent(this, MediaPlayerService.class)); + lifecycleSupport.getValue().onDestroy(); Util.unregisterMediaButtonEventReceiver(this); finish(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java index 7af8157d..0ba31f04 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java @@ -126,7 +126,7 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt List playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this); if (!Util.isOffline(SelectPlaylistActivity.this)) - new CacheCleaner(SelectPlaylistActivity.this, getDownloadService()).cleanPlaylists(playlists); + new CacheCleaner(SelectPlaylistActivity.this).cleanPlaylists(playlists); return playlists; } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java index 43712579..c26f30de 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -29,7 +29,6 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import androidx.appcompat.app.ActionBar; import android.util.Log; import android.view.*; @@ -39,6 +38,7 @@ import android.widget.*; import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.Position; import org.koin.java.standalone.KoinJavaComponent; +import static org.koin.java.standalone.KoinJavaComponent.inject; import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; @@ -56,6 +56,8 @@ import java.io.PrintWriter; import java.util.*; import java.util.regex.Pattern; +import kotlin.Lazy; + /** * @author Sindre Mehus */ @@ -74,6 +76,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen private static final String STATE_ACTIVE_POSITION = "org.moire.ultrasonic.activePosition"; private static final int DIALOG_ASK_FOR_SHARE_DETAILS = 102; + private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + private Lazy lifecycleSupport = inject(DownloadServiceLifecycleSupport.class); + public MenuDrawer menuDrawer; private int activePosition = 1; private int menuActiveViewId; @@ -97,7 +102,6 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen applyTheme(); super.onCreate(bundle); - if (DownloadServiceImpl.getInstance() == null) new DownloadServiceImpl(getApplicationContext()); setVolumeControlStream(AudioManager.STREAM_MUSIC); if (bundle != null) @@ -155,6 +159,8 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen instance = this; Util.registerMediaButtonEventReceiver(this); + // Lifecycle support's constructor registers some event receivers so it should be created early + lifecycleSupport.getValue(); // Make sure to update theme if (theme != null && !theme.equals(Util.getTheme(this))) @@ -256,27 +262,22 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen if (nowPlayingView != null) { - final DownloadService downloadService = DownloadServiceImpl.getInstance(); + PlayerState playerState = downloadServiceImpl.getValue().getPlayerState(); - if (downloadService != null) + if (playerState.equals(PlayerState.PAUSED) || playerState.equals(PlayerState.STARTED)) { - PlayerState playerState = downloadService.getPlayerState(); + DownloadFile file = downloadServiceImpl.getValue().getCurrentPlaying(); - if (playerState.equals(PlayerState.PAUSED) || playerState.equals(PlayerState.STARTED)) + if (file != null) { - DownloadFile file = downloadService.getCurrentPlaying(); - - if (file != null) - { - final Entry song = file.getSong(); - showNowPlaying(SubsonicTabActivity.this, downloadService, song, playerState); - } - } - else - { - hideNowPlaying(); + final Entry song = file.getSong(); + showNowPlaying(SubsonicTabActivity.this, downloadServiceImpl.getValue(), song, playerState); } } + else + { + hideNowPlaying(); + } } return null; @@ -763,18 +764,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen public DownloadService getDownloadService() { - DownloadService downloadService = DownloadServiceImpl.getInstance(); - - if (downloadService != null) - { - return downloadService; - } - - Log.w(TAG, "DownloadService not running. Attempting to start it."); - - new DownloadServiceImpl(getApplicationContext()); - - return DownloadServiceImpl.getInstance(); + return downloadServiceImpl.getValue(); } protected void warnIfNetworkOrStorageUnavailable() diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index 864d8a14..57e780ae 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -22,6 +22,10 @@ import org.moire.ultrasonic.util.*; import java.io.File; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * Shows main app settings. */ @@ -62,6 +66,8 @@ public class SettingsFragment extends PreferenceFragment private SharedPreferences settings; private int activeServers; + private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -401,7 +407,6 @@ public class SettingsFragment extends PreferenceFragment } // Clear download queue. - DownloadService downloadService = DownloadServiceImpl.getInstance(); - downloadService.clear(); + downloadServiceImpl.getValue().clear(); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java index 8a834026..e88db061 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java @@ -18,8 +18,7 @@ import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; -import org.moire.ultrasonic.service.MediaPlayerService; +import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.FileUtil; public class UltraSonicAppWidgetProvider extends AppWidgetProvider @@ -194,27 +193,27 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class); intent.setAction("android.intent.action.MAIN"); intent.addCategory("android.intent.category.LAUNCHER"); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); // Emulate media button clicks. - intent = new Intent("1"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0); views.setOnClickPendingIntent(R.id.control_play, pendingIntent); - intent = new Intent("2"); // Use a unique action name to ensure a different PendingIntent to be created. - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0); views.setOnClickPendingIntent(R.id.control_next, pendingIntent); - intent = new Intent("3"); // Use a unique action name to ensure a different PendingIntent to be created. - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0); views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java index 721f0eb1..85ce1782 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java @@ -8,28 +8,24 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + public class A2dpIntentReceiver extends BroadcastReceiver { - private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse"; + private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); @Override public void onReceive(Context context, Intent intent) { - - DownloadService downloadService = DownloadServiceImpl.getInstance(); - - if (downloadService == null) + if (downloadServiceImpl.getValue().getCurrentPlaying() == null) { return; } - if (downloadService.getCurrentPlaying() == null) - { - return; - } - - Entry song = downloadService.getCurrentPlaying().getSong(); + Entry song = downloadServiceImpl.getValue().getCurrentPlaying().getSong(); if (song == null) { @@ -39,8 +35,8 @@ public class A2dpIntentReceiver extends BroadcastReceiver Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE); Integer duration = song.getDuration(); - Integer playerPosition = downloadService.getPlayerPosition(); - Integer listSize = downloadService.getDownloads().size(); + Integer playerPosition = downloadServiceImpl.getValue().getPlayerPosition(); + Integer listSize = downloadServiceImpl.getValue().getDownloads().size(); if (duration != null) { @@ -50,7 +46,7 @@ public class A2dpIntentReceiver extends BroadcastReceiver avrcpIntent.putExtra("position", (long) playerPosition); avrcpIntent.putExtra("ListSize", (long) listSize); - switch (downloadService.getPlayerState()) + switch (downloadServiceImpl.getValue().getPlayerState()) { case STARTED: avrcpIntent.putExtra("playing", true); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java index 4a731322..4cf7bfe2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java @@ -24,7 +24,7 @@ import android.content.Context; import android.content.Intent; import android.util.Log; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Util; /** @@ -71,7 +71,7 @@ public class BluetoothIntentReceiver extends BroadcastReceiver if (disconnected) { Log.i(TAG, "Disconnected from Bluetooth device, requesting pause."); - context.sendBroadcast(new Intent(DownloadServiceImpl.CMD_PAUSE)); + context.sendBroadcast(new Intent(Constants.CMD_PAUSE)); } } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java index 09593d9e..2315dc3e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java @@ -21,22 +21,25 @@ package org.moire.ultrasonic.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.util.Log; -import org.moire.ultrasonic.service.DownloadServiceImpl; -import org.moire.ultrasonic.service.MediaPlayerService; +import org.moire.ultrasonic.service.DownloadServiceLifecycleSupport; +import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Util; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * @author Sindre Mehus */ public class MediaButtonIntentReceiver extends BroadcastReceiver { - private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); + private Lazy lifecycleSupport = inject(DownloadServiceLifecycleSupport.class); @Override public void onReceive(Context context, Intent intent) @@ -57,23 +60,17 @@ public class MediaButtonIntentReceiver extends BroadcastReceiver Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT); Log.i(TAG, "Got MEDIA_BUTTON key event: " + event); - Intent serviceIntent = new Intent(context, MediaPlayerService.class); - serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); - try { - if (DownloadServiceImpl.getInstance() == null) new DownloadServiceImpl(context); - DownloadServiceImpl.getInstance().onCommand(serviceIntent); + Intent serviceIntent = new Intent(Constants.CMD_PROCESS_KEYCODE); + serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + lifecycleSupport.getValue().receiveIntent(serviceIntent); if (isOrderedBroadcast()) { abortBroadcast(); } } - catch (IllegalStateException exception) - { - Log.w(TAG, "MediaButtonIntentReceiver couldn't start DownloadServiceImpl because the application was in the background."); - } catch (Exception x) { // Ignored. diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java new file mode 100644 index 00000000..4fd5c473 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java @@ -0,0 +1,6 @@ +package org.moire.ultrasonic.service; + +public abstract class Consumer +{ + public abstract void accept(T t); +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java index 22e25e15..2d4c7646 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java @@ -37,11 +37,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; +import kotlin.Lazy; import kotlin.Pair; import static android.content.Context.POWER_SERVICE; import static android.os.PowerManager.ON_AFTER_RELEASE; import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK; +import static org.koin.java.standalone.KoinJavaComponent.inject; /** * @author Sindre Mehus @@ -439,7 +441,7 @@ public class DownloadFile wifiLock.release(); } - new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace(); + new CacheCleaner(context).cleanSpace(); MediaPlayerService.checkDownloads(context); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadQueueSerializer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadQueueSerializer.java new file mode 100644 index 00000000..570ab067 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadQueueSerializer.java @@ -0,0 +1,105 @@ +package org.moire.ultrasonic.service; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; + +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; + +public class DownloadQueueSerializer +{ + private static final String TAG = DownloadQueueSerializer.class.getSimpleName(); + + 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 songs, int currentPlayingIndex, int currentPlayingPosition) + { + if (!setup.get()) + { + return; + } + + new SerializeTask().execute(songs, currentPlayingIndex, currentPlayingPosition); + } + + public void serializeDownloadQueueNow(Iterable songs, int currentPlayingIndex, int currentPlayingPosition) + { + State state = new State(); + for (DownloadFile downloadFile : songs) + { + state.songs.add(downloadFile.getSong()); + } + state.currentPlayingIndex = currentPlayingIndex; + state.currentPlayingPosition = currentPlayingPosition; + + Log.i(TAG, String.format("Serialized currentPlayingIndex: %d, currentPlayingPosition: %d", state.currentPlayingIndex, state.currentPlayingPosition)); + FileUtil.serialize(context, state, Constants.FILENAME_DOWNLOADS_SER); + } + + public void deserializeDownloadQueue(Consumer afterDeserialized) + { + new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, afterDeserialized); + } + + public void deserializeDownloadQueueNow(Consumer afterDeserialized) + { + State state = FileUtil.deserialize(context, Constants.FILENAME_DOWNLOADS_SER); + if (state == null) return; + Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); + afterDeserialized.accept(state); + } + + private class SerializeTask extends AsyncTask + { + @Override + protected Void doInBackground(Object... params) + { + if (lock.tryLock()) + { + try + { + Thread.currentThread().setName("SerializeTask"); + serializeDownloadQueueNow((Iterable)params[0], (int)params[1], (int)params[2]); + } + finally + { + lock.unlock(); + } + } + return null; + } + } + + private class DeserializeTask extends AsyncTask + { + @Override + protected Void doInBackground(Object... params) + { + try + { + Thread.currentThread().setName("DeserializeTask"); + lock.lock(); + deserializeDownloadQueueNow((Consumer)params[0]); + setup.set(true); + } + finally + { + lock.unlock(); + } + + return null; + } + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java index 47b37406..ba3e1de0 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java @@ -20,7 +20,6 @@ package org.moire.ultrasonic.service; import android.content.Context; import android.content.Intent; -import android.os.PowerManager; import android.util.Log; import org.koin.java.standalone.KoinJavaComponent; @@ -42,6 +41,9 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; import static org.moire.ultrasonic.service.MediaPlayerService.playerState; /** @@ -52,26 +54,18 @@ public class DownloadServiceImpl implements DownloadService { private static final String TAG = DownloadServiceImpl.class.getSimpleName(); - public static final String CMD_PLAY = "org.moire.ultrasonic.CMD_PLAY"; - public static final String CMD_TOGGLEPAUSE = "org.moire.ultrasonic.CMD_TOGGLEPAUSE"; - public static final String CMD_PAUSE = "org.moire.ultrasonic.CMD_PAUSE"; - public static final String CMD_STOP = "org.moire.ultrasonic.CMD_STOP"; - public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS"; - public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT"; + private final LRUCache downloadFileCache = new LRUCache<>(100); - private DownloadServiceLifecycleSupport lifecycleSupport; - - private final LRUCache downloadFileCache = new LRUCache(100); - - private static DownloadServiceImpl instance; private String suggestedPlaylistName; private boolean keepScreenOn; private boolean showVisualization; - private boolean jukeboxEnabled; private boolean autoPlayStart; private Context context; + public Lazy jukeboxService = inject(JukeboxService.class); + private Lazy downloadQueueSerializer = inject(DownloadQueueSerializer.class); + private Lazy externalStorageMonitor = inject(ExternalStorageMonitor.class); public DownloadServiceImpl(Context context) { @@ -79,25 +73,152 @@ public class DownloadServiceImpl implements DownloadService // TODO: refactor MediaPlayerService.shufflePlayBuffer = new ShufflePlayBuffer(context); - MediaPlayerService.jukeboxService = new JukeboxService(context, this); + externalStorageMonitor.getValue().onCreate(new Runnable() { + @Override + public void run() { + reset(); + } + }); - instance = this; - lifecycleSupport = new DownloadServiceLifecycleSupport(context,this); - lifecycleSupport.onCreate(); - MediaPlayerService.lifecycleSupport = lifecycleSupport; + int instance = Util.getActiveServer(context); + setJukeboxEnabled(Util.getJukeboxEnabled(context, instance)); Log.i(TAG, "DownloadServiceImpl created"); } - public void onCommand(Intent intent) + public void onDestroy() { - lifecycleSupport.onStart(intent); - Log.i(TAG, "DownloadServiceImpl received intent"); + externalStorageMonitor.getValue().onDestroy(); + context.stopService(new Intent(context, MediaPlayerService.class)); + Log.i(TAG, "DownloadServiceImpl destroyed"); } - public static DownloadServiceImpl getInstance() + private void executeOnStartedMediaPlayerService(final Consumer taskToExecute) { - return instance; + Thread t = new Thread() + { + public void run() + { + MediaPlayerService instance = MediaPlayerService.getInstance(context); + taskToExecute.accept(instance); + } + }; + t.start(); + } + + @Override + public void restore(List songs, final int currentPlayingIndex, final int currentPlayingPosition, final boolean autoPlay, boolean newPlaylist) + { + download(songs, false, false, false, false, newPlaylist); + + if (currentPlayingIndex != -1) + { + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.play(currentPlayingIndex, autoPlayStart); + } + }); + + if (MediaPlayerService.currentPlaying != null) + { + if (autoPlay && jukeboxService.getValue().isEnabled()) + { + jukeboxService.getValue().skip(getCurrentPlayingIndex(), currentPlayingPosition / 1000); + } + else + { + if (MediaPlayerService.currentPlaying.isCompleteFileAvailable()) + { + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.doPlay(MediaPlayerService.currentPlaying, currentPlayingPosition, autoPlay); + } + }); + } + } + } + + autoPlayStart = false; + } + } + + @Override + public synchronized void play(final int index) + { + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.play(index, true); + } + }); + } + + public synchronized void play() + { + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.play(); + } + }); + } + + @Override + public synchronized void togglePlayPause() + { + if (playerState == PlayerState.IDLE) autoPlayStart = true; + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.togglePlayPause(); + } + }); + } + + @Override + public synchronized void seekTo(final int position) + { + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.seekTo(position); + } + }); + } + + @Override + public synchronized void pause() + { + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.pause(); + } + }); + } + + @Override + public synchronized void start() + { + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.start(); + } + }); + } + + @Override + public synchronized void stop() + { + executeOnStartedMediaPlayerService(new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.stop(); + } + }); } @Override @@ -129,8 +250,6 @@ public class DownloadServiceImpl implements DownloadService MediaPlayerService.downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); offset++; } - - MediaPlayerService.revision++; } else { @@ -149,10 +268,10 @@ public class DownloadServiceImpl implements DownloadService if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); } - MediaPlayerService.revision++; } + MediaPlayerService.revision++; - MediaPlayerService.updateJukeboxPlaylist(); + jukeboxService.getValue().updatePlaylist(); if (shuffle) shuffle(); @@ -171,7 +290,7 @@ public class DownloadServiceImpl implements DownloadService MediaPlayerService.checkDownloads(context); } - lifecycleSupport.serializeDownloadQueue(); + downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); } @Override @@ -186,35 +305,7 @@ public class DownloadServiceImpl implements DownloadService MediaPlayerService.revision++; MediaPlayerService.checkDownloads(context); - lifecycleSupport.serializeDownloadQueue(); - } - - @Override - public void restore(List songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist) - { - download(songs, false, false, false, false, newPlaylist); - - if (currentPlayingIndex != -1) - { - MediaPlayerService.getInstance(context).play(currentPlayingIndex, autoPlayStart); - - if (MediaPlayerService.currentPlaying != null) - { - if (autoPlay && jukeboxEnabled) - { - MediaPlayerService.jukeboxService.skip(getCurrentPlayingIndex(), currentPlayingPosition / 1000); - } - else - { - if (MediaPlayerService.currentPlaying.isCompleteFileAvailable()) - { - MediaPlayerService.getInstance(context).doPlay(MediaPlayerService.currentPlaying, currentPlayingPosition, autoPlay); - } - } - } - - autoPlayStart = false; - } + downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); } public synchronized void setCurrentPlaying(DownloadFile currentPlaying) @@ -238,13 +329,13 @@ public class DownloadServiceImpl implements DownloadService @Override public void stopJukeboxService() { - MediaPlayerService.jukeboxService.stopJukeboxService(); + jukeboxService.getValue().stopJukeboxService(); } @Override public void startJukeboxService() { - MediaPlayerService.jukeboxService.startJukeboxService(); + jukeboxService.getValue().startJukeboxService(); } @Override @@ -274,8 +365,8 @@ public class DownloadServiceImpl implements DownloadService MediaPlayerService.downloadList.add(0, MediaPlayerService.currentPlaying); } MediaPlayerService.revision++; - lifecycleSupport.serializeDownloadQueue(); - MediaPlayerService.updateJukeboxPlaylist(); + downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + jukeboxService.getValue().updatePlaylist(); MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); @@ -349,12 +440,17 @@ public class DownloadServiceImpl implements DownloadService @Override public synchronized void clear() { - MediaPlayerService.clear(true); + clear(true); } public synchronized void clear(boolean serialize) { MediaPlayerService.clear(serialize); + jukeboxService.getValue().updatePlaylist(); + if (serialize) + { + downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + } } @Override @@ -383,8 +479,8 @@ public class DownloadServiceImpl implements DownloadService } } - lifecycleSupport.serializeDownloadQueue(); - MediaPlayerService.updateJukeboxPlaylist(); + downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + jukeboxService.getValue().updatePlaylist(); } @Override @@ -415,8 +511,8 @@ public class DownloadServiceImpl implements DownloadService MediaPlayerService.downloadList.remove(downloadFile); MediaPlayerService.backgroundDownloadList.remove(downloadFile); MediaPlayerService.revision++; - lifecycleSupport.serializeDownloadQueue(); - MediaPlayerService.updateJukeboxPlaylist(); + downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + jukeboxService.getValue().updatePlaylist(); if (downloadFile == MediaPlayerService.nextPlaying) { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); @@ -461,10 +557,7 @@ public class DownloadServiceImpl implements DownloadService } @Override - public List getSongs() - { - return MediaPlayerService.downloadList; - } + public List getSongs() { return MediaPlayerService.downloadList; } @Override public long getDownloadListDuration() @@ -495,7 +588,7 @@ public class DownloadServiceImpl implements DownloadService @Override public synchronized List getDownloads() { - List temp = new ArrayList(); + List temp = new ArrayList<>(); temp.addAll(MediaPlayerService.downloadList); temp.addAll(MediaPlayerService.backgroundDownloadList); return temp; @@ -507,33 +600,6 @@ public class DownloadServiceImpl implements DownloadService return MediaPlayerService.backgroundDownloadList; } - @Override - public synchronized void play(int index) - { - MediaPlayerService.getInstance(context).play(index, true); - } - - public synchronized void play() - { - MediaPlayerService.getInstance(context).play(); - } - - /** - * Plays or resumes the playback, depending on the current player state. - */ - @Override - public synchronized void togglePlayPause() - { - if (playerState == PlayerState.IDLE) autoPlayStart = true; - MediaPlayerService.getInstance(context).togglePlayPause(); - } - - @Override - public synchronized void seekTo(int position) - { - MediaPlayerService.getInstance(context).seekTo(position); - } - @Override public synchronized void previous() { @@ -564,24 +630,6 @@ public class DownloadServiceImpl implements DownloadService } } - @Override - public synchronized void pause() - { - MediaPlayerService.getInstance(context).pause(); - } - - @Override - public synchronized void stop() - { - MediaPlayerService.getInstance(context).stop(); - } - - @Override - public synchronized void start() - { - MediaPlayerService.getInstance(context).start(); - } - @Override public synchronized void reset() { @@ -659,18 +707,16 @@ public class DownloadServiceImpl implements DownloadService @Override public boolean isJukeboxEnabled() { - return jukeboxEnabled; + return jukeboxService.getValue().isEnabled(); } @Override public boolean isJukeboxAvailable() { - MusicService musicService = MusicServiceFactory.getMusicService(context); - try { String username = Util.getUserName(context, Util.getActiveServer(context)); - UserInfo user = musicService.getUser(username, context, null); + UserInfo user = MusicServiceFactory.getMusicService(context).getUser(username, context, null); return user.getJukeboxRole(); } catch (Exception e) @@ -684,12 +730,10 @@ public class DownloadServiceImpl implements DownloadService @Override public boolean isSharingAvailable() { - MusicService musicService = MusicServiceFactory.getMusicService(context); - try { String username = Util.getUserName(context, Util.getActiveServer(context)); - UserInfo user = musicService.getUser(username, context, null); + UserInfo user = MusicServiceFactory.getMusicService(context).getUser(username, context, null); return user.getShareRole(); } catch (Exception e) @@ -703,12 +747,12 @@ public class DownloadServiceImpl implements DownloadService @Override public void setJukeboxEnabled(boolean jukeboxEnabled) { - this.jukeboxEnabled = jukeboxEnabled; - MediaPlayerService.jukeboxService.setEnabled(jukeboxEnabled); + jukeboxService.getValue().setEnabled(jukeboxEnabled); + setPlayerState(PlayerState.IDLE); if (jukeboxEnabled) { - MediaPlayerService.jukeboxService.startJukeboxService(); + jukeboxService.getValue().startJukeboxService(); reset(); @@ -720,14 +764,14 @@ public class DownloadServiceImpl implements DownloadService } else { - MediaPlayerService.jukeboxService.stopJukeboxService(); + jukeboxService.getValue().stopJukeboxService(); } } @Override public void adjustJukeboxVolume(boolean up) { - MediaPlayerService.jukeboxService.adjustVolume(up); + jukeboxService.getValue().adjustVolume(up); } @Override @@ -756,9 +800,9 @@ public class DownloadServiceImpl implements DownloadService DownloadFile movedSong = list.remove(from); list.add(to, movedSong); - if (jukeboxEnabled && mainList) + if (jukeboxService.getValue().isEnabled() && mainList) { - MediaPlayerService.updateJukeboxPlaylist(); + jukeboxService.getValue().updatePlaylist(); } else if (mainList && (movedSong == MediaPlayerService.nextPlaying || (currentPlayingIndex + 1) == to)) { @@ -797,11 +841,9 @@ public class DownloadServiceImpl implements DownloadService @Override public void run() { - final MusicService musicService = MusicServiceFactory.getMusicService(context); - try { - musicService.setRating(song.getId(), rating, context, null); + MusicServiceFactory.getMusicService(context).setRating(song.getId(), rating, context, null); } catch (Exception e) { @@ -812,9 +854,4 @@ public class DownloadServiceImpl implements DownloadService updateNotification(); } - - private void handleError(Exception x) - { - Log.w(TAG, String.format("Media player error: %s", x), x); - } } \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java index 4cbcef74..a8ce6468 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java @@ -25,168 +25,80 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.media.AudioManager; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; import android.util.Log; import android.view.KeyEvent; import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.util.CacheCleaner; -import org.moire.ultrasonic.util.FileUtil; +import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Util; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; /** * @author Sindre Mehus */ public class DownloadServiceLifecycleSupport { - private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName(); - private static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser"; - private final DownloadServiceImpl downloadService; - private ScheduledExecutorService executorService; + private Lazy downloadQueueSerializer = inject(DownloadQueueSerializer.class); + private final DownloadServiceImpl downloadService; // From DI + private BroadcastReceiver headsetEventReceiver; - private BroadcastReceiver ejectEventReceiver; - private PhoneStateListener phoneStateListener; - private boolean externalStorageAvailable = true; - private Lock lock = new ReentrantLock(); - private final AtomicBoolean setup = new AtomicBoolean(false); private Context context; - /** - * This receiver manages the intent that could come from other applications. - */ - private BroadcastReceiver intentReceiver = new BroadcastReceiver() - { - @Override - public void onReceive(Context context, Intent intent) - { - String action = intent.getAction(); - Log.i(TAG, "intentReceiver.onReceive: " + action); - if (DownloadServiceImpl.CMD_PLAY.equals(action)) - { - downloadService.play(); - } - else if (DownloadServiceImpl.CMD_NEXT.equals(action)) - { - downloadService.next(); - } - else if (DownloadServiceImpl.CMD_PREVIOUS.equals(action)) - { - downloadService.previous(); - } - else if (DownloadServiceImpl.CMD_TOGGLEPAUSE.equals(action)) - { - downloadService.togglePlayPause(); - } - else if (DownloadServiceImpl.CMD_PAUSE.equals(action)) - { - downloadService.pause(); - } - else if (DownloadServiceImpl.CMD_STOP.equals(action)) - { - downloadService.pause(); - downloadService.seekTo(0); - } - } - }; - - - public DownloadServiceLifecycleSupport(Context context, DownloadServiceImpl downloadService) + public DownloadServiceLifecycleSupport(Context context, final DownloadServiceImpl downloadService) { this.downloadService = downloadService; this.context = context; - } - public void onCreate() - { - Runnable downloadChecker = new Runnable() - { - @Override - public void run() - { - try - { - MediaPlayerService.checkDownloads(context); - } - catch (Throwable x) - { - Log.e(TAG, "checkDownloads() failed.", x); - } - } - }; - - executorService = Executors.newSingleThreadScheduledExecutor(); - executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS); - - registerHeadsetReceiver(); - - // Stop when SD card is ejected. - ejectEventReceiver = new BroadcastReceiver() - { - @Override - public void onReceive(Context context, Intent intent) - { - externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction()); - if (!externalStorageAvailable) - { - Log.i(TAG, "External media is ejecting. Stopping playback."); - downloadService.reset(); - } - else - { - Log.i(TAG, "External media is available."); - } - } - }; - IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); - ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); - ejectFilter.addDataScheme("file"); - context.registerReceiver(ejectEventReceiver, ejectFilter); + registerHeadsetReceiver(); // React to media buttons. Util.registerMediaButtonEventReceiver(context); - // Pause temporarily on incoming phone calls. - //phoneStateListener = new MyPhoneStateListener(); - //TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); - //telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - // Register the handler for outside intents. IntentFilter commandFilter = new IntentFilter(); - commandFilter.addAction(DownloadServiceImpl.CMD_PLAY); - commandFilter.addAction(DownloadServiceImpl.CMD_TOGGLEPAUSE); - commandFilter.addAction(DownloadServiceImpl.CMD_PAUSE); - commandFilter.addAction(DownloadServiceImpl.CMD_STOP); - commandFilter.addAction(DownloadServiceImpl.CMD_PREVIOUS); - commandFilter.addAction(DownloadServiceImpl.CMD_NEXT); + commandFilter.addAction(Constants.CMD_PLAY); + commandFilter.addAction(Constants.CMD_TOGGLEPAUSE); + commandFilter.addAction(Constants.CMD_PAUSE); + commandFilter.addAction(Constants.CMD_STOP); + commandFilter.addAction(Constants.CMD_PREVIOUS); + commandFilter.addAction(Constants.CMD_NEXT); + commandFilter.addAction(Constants.CMD_PROCESS_KEYCODE); context.registerReceiver(intentReceiver, commandFilter); - int instance = Util.getActiveServer(context); - downloadService.setJukeboxEnabled(Util.getJukeboxEnabled(context, instance)); + downloadQueueSerializer.getValue().deserializeDownloadQueue(new Consumer() { + @Override + public void accept(State state) { + downloadService.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition, false, false); - deserializeDownloadQueue(); + // Work-around: Serialize again, as the restore() method creates a serialization without current playing info. + downloadQueueSerializer.getValue().serializeDownloadQueue(downloadService.getSongs(), + downloadService.getCurrentPlayingIndex(), downloadService.getPlayerPosition()); + } + }); - new CacheCleaner(context, downloadService).clean(); + new CacheCleaner(context).clean(); + Log.i(TAG, "LifecycleSupport created"); } - private void registerHeadsetReceiver() { + public void onDestroy() + { + downloadService.clear(false); + context.unregisterReceiver(headsetEventReceiver); + context.unregisterReceiver(intentReceiver); + downloadService.onDestroy(); + Log.i(TAG, "LifecycleSupport destroyed"); + } + + private void registerHeadsetReceiver() { // Pause when headset is unplugged. final SharedPreferences sp = Util.getPreferences(context); final String spKey = context @@ -223,8 +135,9 @@ public class DownloadServiceLifecycleSupport context.registerReceiver(headsetEventReceiver, headsetIntentFilter); } - public void onStart(Intent intent) + public void receiveIntent(Intent intent) { + Log.i(TAG, "Received intent"); if (intent != null && intent.getExtras() != null) { KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); @@ -235,66 +148,6 @@ public class DownloadServiceLifecycleSupport } } - public void onDestroy() - { - executorService.shutdown(); - serializeDownloadQueueNow(); - downloadService.clear(false); - context.unregisterReceiver(ejectEventReceiver); - context.unregisterReceiver(headsetEventReceiver); - context.unregisterReceiver(intentReceiver); - } - - public boolean isExternalStorageAvailable() - { - return externalStorageAvailable; - } - - public void serializeDownloadQueue() - { - if (!setup.get()) - { - return; - } - - new SerializeTask().execute(); - } - - public void serializeDownloadQueueNow() - { - Iterable songs = new ArrayList(downloadService.getSongs()); - State state = new State(); - for (DownloadFile downloadFile : songs) - { - state.songs.add(downloadFile.getSong()); - } - state.currentPlayingIndex = downloadService.getCurrentPlayingIndex(); - state.currentPlayingPosition = downloadService.getPlayerPosition(); - - Log.i(TAG, String.format("Serialized currentPlayingIndex: %d, currentPlayingPosition: %d", state.currentPlayingIndex, state.currentPlayingPosition)); - FileUtil.serialize(context, state, FILENAME_DOWNLOADS_SER); - } - - private void deserializeDownloadQueue() - { - new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void deserializeDownloadQueueNow() - { - State state = FileUtil.deserialize(context, FILENAME_DOWNLOADS_SER); - if (state == null) - { - return; - } - Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); - // TODO: here the autoPlay = false creates problems when Ultrasonic is started by a Play MediaButton as the player won't start this way. - downloadService.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition, false, false); - - // Work-around: Serialize again, as the restore() method creates a serialization without current playing info. - serializeDownloadQueue(); - } - private void handleKeyEvent(KeyEvent event) { if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0) @@ -321,11 +174,7 @@ public class DownloadServiceLifecycleSupport downloadService.stop(); break; case KeyEvent.KEYCODE_MEDIA_PLAY: - if (downloadService.getPlayerState() == PlayerState.IDLE) - { - downloadService.play(); - } - else if (downloadService.getPlayerState() != PlayerState.STARTED) + if (downloadService.getPlayerState() != PlayerState.STARTED) { downloadService.start(); } @@ -354,87 +203,39 @@ public class DownloadServiceLifecycleSupport } /** - * Logic taken from packages/apps/Music. Will pause when an incoming - * call rings or if a call (incoming or outgoing) is connected. + * This receiver manages the intent that could come from other applications. */ - private class MyPhoneStateListener extends PhoneStateListener + private BroadcastReceiver intentReceiver = new BroadcastReceiver() { - private boolean resumeAfterCall; - @Override - public void onCallStateChanged(int state, String incomingNumber) + public void onReceive(Context context, Intent intent) { - switch (state) + String action = intent.getAction(); + if (action == null) return; + Log.i(TAG, "intentReceiver.onReceive: " + action); + + switch(action) { - case TelephonyManager.CALL_STATE_RINGING: - case TelephonyManager.CALL_STATE_OFFHOOK: - if (downloadService.getPlayerState() == PlayerState.STARTED && !downloadService.isJukeboxEnabled()) - { - resumeAfterCall = true; - downloadService.pause(); - } + case Constants.CMD_PLAY: + downloadService.play(); break; - case TelephonyManager.CALL_STATE_IDLE: - if (resumeAfterCall) - { - resumeAfterCall = false; - downloadService.start(); - } + case Constants.CMD_NEXT: + downloadService.next(); break; - default: + case Constants.CMD_PREVIOUS: + downloadService.previous(); + break; + case Constants.CMD_TOGGLEPAUSE: + downloadService.togglePlayPause(); + break; + case Constants.CMD_STOP: + downloadService.pause(); + downloadService.seekTo(0); + break; + case Constants.CMD_PROCESS_KEYCODE: + receiveIntent(intent); break; } } - } - - private static class State implements Serializable - { - private static final long serialVersionUID = -6346438781062572270L; - - private List songs = new ArrayList(); - private int currentPlayingIndex; - private int currentPlayingPosition; - } - - private class SerializeTask extends AsyncTask - { - @Override - protected Void doInBackground(Void... params) - { - if (lock.tryLock()) - { - try - { - Thread.currentThread().setName("SerializeTask"); - serializeDownloadQueueNow(); - } - finally - { - lock.unlock(); - } - } - return null; - } - } - - private class DeserializeTask extends AsyncTask - { - @Override - protected Void doInBackground(Void... params) - { - try - { - Thread.currentThread().setName("DeserializeTask"); - lock.lock(); - deserializeDownloadQueueNow(); - setup.set(true); - } - finally - { - lock.unlock(); - } - - return null; - } - } + }; } \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java new file mode 100644 index 00000000..489eb39b --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java @@ -0,0 +1,55 @@ +package org.moire.ultrasonic.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +public class ExternalStorageMonitor +{ + private static final String TAG = ExternalStorageMonitor.class.getSimpleName(); + + private Context context; + private BroadcastReceiver ejectEventReceiver; + private boolean externalStorageAvailable = true; + + public ExternalStorageMonitor(Context context) + { + this.context = context; + } + + public void onCreate(final Runnable ejectedCallback) + { + // Stop when SD card is ejected. + ejectEventReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction()); + if (!externalStorageAvailable) + { + Log.i(TAG, "External media is ejecting. Stopping playback."); + ejectedCallback.run(); + } + else + { + Log.i(TAG, "External media is available."); + } + } + }; + + IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); + ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + ejectFilter.addDataScheme("file"); + context.registerReceiver(ejectEventReceiver, ejectFilter); + } + + public void onDestroy() + { + context.unregisterReceiver(ejectEventReceiver); + } + + public boolean isExternalStorageAvailable() { return externalStorageAvailable; } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java index cc321cb3..afbda458 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java @@ -44,6 +44,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * Provides an asynchronous interface to the remote jukebox on the Subsonic server. * @@ -52,13 +56,10 @@ import java.util.concurrent.atomic.AtomicLong; */ public class JukeboxService { - private static final String TAG = JukeboxService.class.getSimpleName(); private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L; - private final Handler handler = new Handler(); private final TaskQueue tasks = new TaskQueue(); - private final DownloadServiceImpl downloadService; private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture statusUpdateFuture; private final AtomicLong timeOfLastUpdate = new AtomicLong(); @@ -67,18 +68,20 @@ public class JukeboxService private VolumeToast volumeToast; private AtomicBoolean running = new AtomicBoolean(); private Thread serviceThread; + private boolean enabled = false; private Context context; + private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + // TODO: Report warning if queue fills up. // TODO: Create shutdown method? // TODO: Disable repeat. // TODO: Persist RC state? // TODO: Minimize status updates. - public JukeboxService(Context context, DownloadServiceImpl downloadService) + public JukeboxService(Context context) { this.context = context; - this.downloadService = downloadService; } public void startJukeboxService() @@ -179,9 +182,9 @@ public class JukeboxService // Track change? Integer index = jukeboxStatus.getCurrentPlayingIndex(); - if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) + if (index != null && index != -1 && index != downloadServiceImpl.getValue().getCurrentPlayingIndex()) { - downloadService.setCurrentPlaying(index); + downloadServiceImpl.getValue().setCurrentPlaying(index); } } @@ -209,7 +212,7 @@ public class JukeboxService { Log.w(TAG, x.toString()); - handler.post(new Runnable() + new Handler().post(new Runnable() { @Override public void run() @@ -218,17 +221,19 @@ public class JukeboxService } }); - downloadService.setJukeboxEnabled(false); + downloadServiceImpl.getValue().setJukeboxEnabled(false); } public void updatePlaylist() { + if (!enabled) return; + tasks.remove(Skip.class); tasks.remove(Stop.class); tasks.remove(Start.class); List ids = new ArrayList(); - for (DownloadFile file : downloadService.getDownloads()) + for (DownloadFile file : downloadServiceImpl.getValue().getDownloads()) { ids.add(file.getSong().getId()); } @@ -250,7 +255,7 @@ public class JukeboxService } tasks.add(new Skip(index, offsetSeconds)); - downloadService.setPlayerState(PlayerState.STARTED); + downloadServiceImpl.getValue().setPlayerState(PlayerState.STARTED); } public void stop() @@ -320,8 +325,11 @@ public class JukeboxService } stop(); + } - downloadService.setPlayerState(PlayerState.IDLE); + public boolean isEnabled() + { + return enabled; } private static class TaskQueue diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 0d44ab69..2797f967 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -32,6 +32,7 @@ import androidx.core.app.NotificationManagerCompat; import org.koin.java.standalone.KoinJavaComponent; import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.DownloadActivity; +import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.audiofx.EqualizerController; import org.moire.ultrasonic.audiofx.VisualizerController; @@ -58,7 +59,13 @@ import java.net.URLEncoder; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; import static org.moire.ultrasonic.domain.PlayerState.IDLE; @@ -101,9 +108,11 @@ public class MediaPlayerService extends Service public static DownloadFile currentDownloading; public static DownloadFile nextPlaying; - public static boolean jukeboxEnabled; - public static JukeboxService jukeboxService; - public static DownloadServiceLifecycleSupport lifecycleSupport; + public Lazy jukeboxService = inject(JukeboxService.class); + private Lazy downloadQueueSerializer = inject(DownloadQueueSerializer.class); + private Lazy externalStorageMonitor = inject(ExternalStorageMonitor.class); + + private ScheduledExecutorService executorService; public static int cachedPosition; private PositionCache positionCache; @@ -233,6 +242,25 @@ public class MediaPlayerService extends Service } }).start(); + Runnable downloadChecker = new Runnable() + { + @Override + public void run() + { + try + { + MediaPlayerService.checkDownloads(MediaPlayerService.this); + } + catch (Throwable x) + { + Log.e(TAG, "checkDownloads() failed.", x); + } + } + }; + + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS); + audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); setUpRemoteControlClient(); @@ -274,7 +302,8 @@ public class MediaPlayerService extends Service // We should use a single notification builder, otherwise the notification may not be updated notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); - + // Update notification early. It is better to show an empty one temporarily than waiting too long and letting Android kill the app + updateNotification(); instance = this; Log.i(TAG, "MediaPlayerService created"); @@ -284,7 +313,6 @@ public class MediaPlayerService extends Service public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); - lifecycleSupport.onStart(intent); return START_NOT_STICKY; } @@ -296,6 +324,8 @@ public class MediaPlayerService extends Service instance = null; reset(); + executorService.shutdown(); + try { mediaPlayer.release(); @@ -501,9 +531,9 @@ public class MediaPlayerService extends Service { try { - if (jukeboxEnabled) + if (jukeboxService.getValue().isEnabled()) { - jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); + jukeboxService.getValue().skip(getCurrentPlayingIndex(), position / 1000); } else { @@ -528,7 +558,7 @@ public class MediaPlayerService extends Service return 0; } - return jukeboxEnabled ? jukeboxService.getPositionSeconds() * 1000 : cachedPosition; + return jukeboxService.getValue().isEnabled() ? jukeboxService.getValue().getPositionSeconds() * 1000 : cachedPosition; } catch (Exception x) { @@ -736,9 +766,9 @@ public class MediaPlayerService extends Service if (start) { - if (jukeboxEnabled) + if (jukeboxService.getValue().isEnabled()) { - jukeboxService.skip(getCurrentPlayingIndex(), 0); + jukeboxService.getValue().skip(getCurrentPlayingIndex(), 0); setPlayerState(STARTED); } else @@ -756,7 +786,7 @@ public class MediaPlayerService extends Service { reset(); setCurrentPlaying(null); - lifecycleSupport.serializeDownloadQueue(); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); } public synchronized void reset() @@ -802,9 +832,9 @@ public class MediaPlayerService extends Service { if (playerState == STARTED) { - if (jukeboxEnabled) + if (jukeboxService.getValue().isEnabled()) { - jukeboxService.stop(); + jukeboxService.getValue().stop(); } else { @@ -825,9 +855,9 @@ public class MediaPlayerService extends Service { if (playerState == STARTED) { - if (jukeboxEnabled) + if (jukeboxService.getValue().isEnabled()) { - jukeboxService.stop(); + jukeboxService.getValue().stop(); } else { @@ -846,9 +876,9 @@ public class MediaPlayerService extends Service { try { - if (jukeboxEnabled) + if (jukeboxService.getValue().isEnabled()) { - jukeboxService.start(); + jukeboxService.getValue().start(); } else { @@ -885,7 +915,7 @@ public class MediaPlayerService extends Service if (playerState == PAUSED) { - lifecycleSupport.serializeDownloadQueue(); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); } if (playerState == PlayerState.STARTED) @@ -1080,7 +1110,7 @@ public class MediaPlayerService extends Service } } - lifecycleSupport.serializeDownloadQueue(); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); } }); @@ -1274,6 +1304,8 @@ public class MediaPlayerService extends Service if (Util.getShouldClearPlaylist(this)) { clear(true); + jukeboxService.getValue().updatePlaylist(); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); } resetPlayback(); @@ -1294,6 +1326,8 @@ public class MediaPlayerService extends Service } } + // TODO: Serialization was originally here, removed for static. refactor this and but back + // downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); public static synchronized void clear(boolean serialize) { MediaPlayerService mediaPlayerService = getRunningInstance(); @@ -1309,19 +1343,14 @@ public class MediaPlayerService extends Service if (mediaPlayerService != null) { mediaPlayerService.setCurrentPlaying(null); - updateJukeboxPlaylist(); mediaPlayerService.setNextPlaying(); } - - if (serialize) - { - lifecycleSupport.serializeDownloadQueue(); - } } protected static synchronized void checkDownloads(Context context) { - if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) + // TODO: refactor inject + if (!Util.isExternalStoragePresent() || !inject(ExternalStorageMonitor.class).getValue().isExternalStorageAvailable()) { return; } @@ -1331,7 +1360,8 @@ public class MediaPlayerService extends Service checkShufflePlay(context); } - if (jukeboxEnabled || !Util.isNetworkConnected(context)) + // TODO: This inject is ugly, refactor + if (inject(JukeboxService.class).getValue().isEnabled() || !Util.isNetworkConnected(context)) { return; } @@ -1472,7 +1502,7 @@ public class MediaPlayerService extends Service if (revisionBefore != revision) { - getInstance(context).updateJukeboxPlaylist(); + getInstance(context).jukeboxService.getValue().updatePlaylist(); } if (wasEmpty && !MediaPlayerService.downloadList.isEmpty()) @@ -1481,14 +1511,6 @@ public class MediaPlayerService extends Service } } - public static void updateJukeboxPlaylist() - { - if (jukeboxEnabled) - { - jukeboxService.updatePlaylist(); - } - } - private static synchronized void cleanup(Context context) { Iterator iterator = cleanupCandidates.iterator(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java index b1ab7bf8..97e3d125 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java @@ -584,12 +584,6 @@ public class OfflineMusicService extends RESTMusicService @Override public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception { - DownloadService downloadService = DownloadServiceImpl.getInstance(); - if (downloadService == null) - { - return new MusicDirectory(); - } - Reader reader = null; BufferedReader buffer = null; try diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/State.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/State.java new file mode 100644 index 00000000..b0c19e90 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/State.java @@ -0,0 +1,16 @@ +package org.moire.ultrasonic.service; + +import org.moire.ultrasonic.domain.MusicDirectory; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class State implements Serializable +{ + public static final long serialVersionUID = -6346438781062572270L; + + public List songs = new ArrayList(); + public int currentPlayingIndex; + public int currentPlayingPosition; +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java index 9d1edf4f..83ccad56 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -8,6 +8,7 @@ import android.util.Log; import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadService; +import org.moire.ultrasonic.service.DownloadServiceImpl; import java.io.File; import java.util.ArrayList; @@ -19,6 +20,10 @@ import java.util.List; import java.util.Set; import java.util.SortedSet; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * @author Sindre Mehus * @version $Id$ @@ -30,12 +35,11 @@ public class CacheCleaner private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L; private final Context context; - private final DownloadService downloadService; + private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); - public CacheCleaner(Context context, DownloadService downloadService) + public CacheCleaner(Context context) { this.context = context; - this.downloadService = downloadService; } public void clean() @@ -219,7 +223,7 @@ public class CacheCleaner { Set filesToNotDelete = new HashSet(5); - for (DownloadFile downloadFile : downloadService.getDownloads()) + for (DownloadFile downloadFile : downloadServiceImpl.getValue().getDownloads()) { filesToNotDelete.add(downloadFile.getPartialFile()); filesToNotDelete.add(downloadFile.getCompleteFile()); @@ -234,12 +238,6 @@ public class CacheCleaner @Override protected Void doInBackground(Void... params) { - if (downloadService == null) - { - Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); - return null; - } - try { Thread.currentThread().setName("BackgroundCleanup"); @@ -268,12 +266,6 @@ public class CacheCleaner @Override protected Void doInBackground(Void... params) { - if (downloadService == null) - { - Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); - return null; - } - try { Thread.currentThread().setName("BackgroundSpaceCleanup"); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java index 827891fb..303bc680 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -60,6 +60,15 @@ public final class Constants public static final String INTENT_EXTRA_NAME_IS_ALBUM = "subsonic.isalbum"; public static final String INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos"; + // Names for Intent Actions + public static final String CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE"; + public static final String CMD_PLAY = "org.moire.ultrasonic.CMD_PLAY"; + public static final String CMD_TOGGLEPAUSE = "org.moire.ultrasonic.CMD_TOGGLEPAUSE"; + public static final String CMD_PAUSE = "org.moire.ultrasonic.CMD_PAUSE"; + public static final String CMD_STOP = "org.moire.ultrasonic.CMD_STOP"; + public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS"; + public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT"; + // Notification IDs. public static final int NOTIFICATION_ID_PLAYING = 100; @@ -140,6 +149,8 @@ public final class Constants // URL for project donations. public static final String DONATION_URL = "http://www.subsonic.org/pages/premium.jsp"; + public static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser"; + public static final String ALBUM_ART_FILE = "folder.jpeg"; public static final String STARRED = "starred"; public static final String ALPHABETICAL_BY_NAME = "alphabeticalByName"; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index 26a2870a..da8b1d9e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -56,6 +56,7 @@ import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.DownloadServiceLifecycleSupport; import org.moire.ultrasonic.service.MediaPlayerService; import org.moire.ultrasonic.service.MusicServiceFactory; @@ -1283,58 +1284,58 @@ public class Util extends DownloadActivity views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); // Emulate media button clicks. - intent = new Intent("1"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 1, intent, 0); views.setOnClickPendingIntent(R.id.control_play, pendingIntent); - intent = new Intent("2"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 2, intent, 0); views.setOnClickPendingIntent(R.id.control_next, pendingIntent); - intent = new Intent("3"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 3, intent, 0); views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); - intent = new Intent("4"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 4, intent, 0); views.setOnClickPendingIntent(R.id.control_stop, pendingIntent); - intent = new Intent("RATE_1"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_1)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 5, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_1, pendingIntent); - intent = new Intent("RATE_2"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_2)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 6, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_2, pendingIntent); - intent = new Intent("RATE_3"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_3)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 7, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_3, pendingIntent); - intent = new Intent("RATE_4"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_4)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 8, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_4, pendingIntent); - intent = new Intent("RATE_5"); - intent.setComponent(new ComponentName(context, MediaPlayerService.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_5)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 9, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_5, pendingIntent); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java index 24460f67..5014c71b 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java @@ -46,6 +46,10 @@ import org.moire.ultrasonic.util.VideoPlayerType; import java.io.File; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * Used to display songs in a {@code ListView}. * @@ -72,13 +76,14 @@ public class SongView extends UpdateView implements Checkable private ImageType leftImageType; private ImageType rightImageType; private Drawable rightImage; - private DownloadService downloadService; private DownloadFile downloadFile; private boolean playing; private EntryAdapter.SongViewHolder viewHolder; private boolean maximized = false; private boolean useFiveStarRating; + private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + public SongView(Context context) { super(context); @@ -164,10 +169,7 @@ public class SongView extends UpdateView implements Checkable this.song = song; - if (downloadService != null) - { - this.downloadFile = downloadService.forSong(song); - } + this.downloadFile = downloadServiceImpl.getValue().forSong(song); StringBuilder artist = new StringBuilder(60); @@ -311,10 +313,6 @@ public class SongView extends UpdateView implements Checkable @Override protected void updateBackground() { - if (downloadService == null) - { - downloadService = DownloadServiceImpl.getInstance(); - } } @Override @@ -322,12 +320,7 @@ public class SongView extends UpdateView implements Checkable { updateBackground(); - if (downloadService == null) - { - return; - } - - downloadFile = downloadService.forSong(this.song); + downloadFile = downloadServiceImpl.getValue().forSong(this.song); File partialFile = downloadFile.getPartialFile(); if (downloadFile.isWorkDone()) @@ -417,7 +410,7 @@ public class SongView extends UpdateView implements Checkable viewHolder.fiveStar4.setImageDrawable(rating > 3 ? starDrawable : starHollowDrawable); viewHolder.fiveStar5.setImageDrawable(rating > 4 ? starDrawable : starHollowDrawable); - boolean playing = downloadService.getCurrentPlaying() == downloadFile; + boolean playing = downloadServiceImpl.getValue().getCurrentPlaying() == downloadFile; if (playing) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java index c8aafa47..92332344 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java @@ -30,6 +30,10 @@ import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * A simple class that draws waveform data received from a * {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture} @@ -39,7 +43,6 @@ import org.moire.ultrasonic.service.DownloadServiceImpl; */ public class VisualizerView extends View { - private static final int PREFERRED_CAPTURE_RATE_MILLIHERTZ = 20000; private final Paint paint = new Paint(); @@ -48,6 +51,8 @@ public class VisualizerView extends View private float[] points; private boolean active; + private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + public VisualizerView(Context context) { super(context); @@ -97,10 +102,9 @@ public class VisualizerView extends View invalidate(); } - private static Visualizer getVizualizer() + private Visualizer getVizualizer() { - DownloadService downloadService = DownloadServiceImpl.getInstance(); - VisualizerController visualizerController = downloadService == null ? null : downloadService.getVisualizerController(); + VisualizerController visualizerController = downloadServiceImpl.getValue().getVisualizerController(); return visualizerController == null ? null : visualizerController.getVisualizer(); } @@ -120,8 +124,7 @@ public class VisualizerView extends View return; } - DownloadService downloadService = DownloadServiceImpl.getInstance(); - if (downloadService != null && downloadService.getPlayerState() != PlayerState.STARTED) + if (downloadServiceImpl.getValue().getPlayerState() != PlayerState.STARTED) { return; } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 193e34d9..eb07d759 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -3,6 +3,7 @@ package org.moire.ultrasonic.di import android.content.SharedPreferences import android.util.Log +import org.koin.android.ext.koin.androidContext import kotlin.math.abs import org.koin.dsl.module.module import org.moire.ultrasonic.BuildConfig @@ -11,10 +12,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration import org.moire.ultrasonic.api.subsonic.di.subsonicApiModule import org.moire.ultrasonic.cache.PermanentFileStorage -import org.moire.ultrasonic.service.CachedMusicService -import org.moire.ultrasonic.service.MusicService -import org.moire.ultrasonic.service.OfflineMusicService -import org.moire.ultrasonic.service.RESTMusicService +import org.moire.ultrasonic.service.* import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader import org.moire.ultrasonic.util.Constants @@ -113,4 +111,10 @@ val musicServiceModule = module(MUSIC_SERVICE_CONTEXT) { } single { SubsonicImageLoader(getProperty(DiProperties.APP_CONTEXT), get()) } + + single { DownloadServiceImpl(androidContext()) } + single { JukeboxService(androidContext()) } + single { DownloadServiceLifecycleSupport(androidContext(), get())} + single { DownloadQueueSerializer(androidContext())} + single { ExternalStorageMonitor(androidContext())} }