From 5eaf9cccb1e78654f3f523c8a3540034a3ac0872 Mon Sep 17 00:00:00 2001 From: Nite Date: Sun, 31 Oct 2021 15:22:15 +0100 Subject: [PATCH 1/7] Started refactoring events to ReactiveX --- dependencies.gradle | 4 ++ ultrasonic/build.gradle | 2 + .../fragment/NowPlayingFragment.java | 6 +-- .../ultrasonic/fragment/SettingsFragment.java | 6 +-- .../ultrasonic/activity/NavigationActivity.kt | 23 +++++----- .../moire/ultrasonic/di/ApplicationModule.kt | 2 - .../service/AutoMediaBrowserService.kt | 13 +++--- .../service/MediaPlayerLifecycleSupport.kt | 20 +++------ .../ultrasonic/service/MediaPlayerService.kt | 29 ++++++------- .../org/moire/ultrasonic/service/RxBus.kt | 42 +++++++++++++++++++ .../util/MediaSessionEventDistributor.kt | 22 ---------- .../util/MediaSessionEventListener.kt | 4 +- .../ultrasonic/util/MediaSessionHandler.kt | 7 ++-- .../util/NowPlayingEventDistributor.kt | 4 -- .../util/NowPlayingEventListener.kt | 1 - .../util/ThemeChangedEventDistributor.kt | 22 ---------- .../util/ThemeChangedEventListener.kt | 8 ---- 17 files changed, 97 insertions(+), 118 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventDistributor.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventListener.kt diff --git a/dependencies.gradle b/dependencies.gradle index a18b5d87..520aa0a4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -43,6 +43,8 @@ ext.versions = [ timber : "4.7.1", fastScroll : "2.0.1", colorPicker : "2.2.3", + rxJava : "3.1.2", + rxAndroid : "3.0.0", ] ext.gradlePlugins = [ @@ -91,6 +93,8 @@ ext.other = [ fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll", sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView", colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker", + rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava", + rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid", ] ext.testing = [ diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 7835f863..27d06bc3 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -106,6 +106,8 @@ dependencies { implementation other.fastScroll implementation other.sortListView implementation other.colorPickerView + implementation other.rxJava + implementation other.rxAndroid kapt androidSupport.room diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java index 4da43426..cd5246cd 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java @@ -18,6 +18,7 @@ import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.MediaPlayerController; +import org.moire.ultrasonic.service.RxBus; import org.moire.ultrasonic.subsonic.ImageLoaderProvider; import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.NowPlayingEventDistributor; @@ -26,6 +27,7 @@ import org.moire.ultrasonic.util.Settings; import org.moire.ultrasonic.util.Util; import kotlin.Lazy; +import kotlin.Unit; import timber.log.Timber; import static org.koin.java.KoinJavaComponent.inject; @@ -70,8 +72,6 @@ public class NowPlayingFragment extends Fragment { nowPlayingArtist = view.findViewById(R.id.now_playing_artist); nowPlayingEventListener = new NowPlayingEventListener() { - @Override - public void onDismissNowPlaying() { } @Override public void onHideNowPlaying() { } @Override @@ -177,7 +177,7 @@ public class NowPlayingFragment extends Fragment { { if (deltaY < 0) { - nowPlayingEventDistributor.getValue().raiseNowPlayingDismissedEvent(); + RxBus.INSTANCE.getDismissNowPlayingCommandPublisher().onNext(Unit.INSTANCE); return false; } if (deltaY > 0) 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 c1fb0466..e70dfc17 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -31,12 +31,12 @@ import org.moire.ultrasonic.log.FileLoggerTree; import org.moire.ultrasonic.provider.SearchSuggestionProvider; import org.moire.ultrasonic.service.Consumer; import org.moire.ultrasonic.service.MediaPlayerController; +import org.moire.ultrasonic.service.RxBus; import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.FileUtil; import org.moire.ultrasonic.util.MediaSessionHandler; import org.moire.ultrasonic.util.PermissionUtil; import org.moire.ultrasonic.util.Settings; -import org.moire.ultrasonic.util.ThemeChangedEventDistributor; import org.moire.ultrasonic.util.TimeSpanPreference; import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat; import org.moire.ultrasonic.util.Util; @@ -44,6 +44,7 @@ import org.moire.ultrasonic.util.Util; import java.io.File; import kotlin.Lazy; +import kotlin.Unit; import timber.log.Timber; import static org.koin.java.KoinJavaComponent.inject; @@ -89,7 +90,6 @@ public class SettingsFragment extends PreferenceFragmentCompat private final Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); private final Lazy permissionUtil = inject(PermissionUtil.class); - private final Lazy themeChangedEventDistributor = inject(ThemeChangedEventDistributor.class); private final Lazy mediaSessionHandler = inject(MediaSessionHandler.class); @Override @@ -192,7 +192,7 @@ public class SettingsFragment extends PreferenceFragmentCompat } else if (Constants.PREFERENCES_KEY_ID3_TAGS.equals(key)) { showArtistPicture.setEnabled(sharedPreferences.getBoolean(key, false)); } else if (Constants.PREFERENCES_KEY_THEME.equals(key)) { - themeChangedEventDistributor.getValue().RaiseThemeChangedEvent(); + RxBus.INSTANCE.getThemeChangedEventPublisher().onNext(Unit.INSTANCE); } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index a92bfcc4..a98b042c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -31,6 +31,7 @@ import androidx.navigation.ui.setupWithNavController import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.google.android.material.navigation.NavigationView +import io.reactivex.rxjava3.disposables.Disposable import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R @@ -43,6 +44,7 @@ import org.moire.ultrasonic.provider.SearchSuggestionProvider import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport +import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil @@ -52,8 +54,6 @@ import org.moire.ultrasonic.util.PermissionUtil import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler -import org.moire.ultrasonic.util.ThemeChangedEventDistributor -import org.moire.ultrasonic.util.ThemeChangedEventListener import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -75,14 +75,13 @@ class NavigationActivity : AppCompatActivity() { private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var nowPlayingEventListener: NowPlayingEventListener - private lateinit var themeChangedEventListener: ThemeChangedEventListener + private var themeChangedEventSubscription: Disposable? = null private val serverSettingsModel: ServerSettingsModel by viewModel() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() private val mediaPlayerController: MediaPlayerController by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject() - private val themeChangedEventDistributor: ThemeChangedEventDistributor by inject() private val permissionUtil: PermissionUtil by inject() private val activeServerProvider: ActiveServerProvider by inject() private val serverRepository: ServerSettingDao by inject() @@ -169,11 +168,12 @@ class NavigationActivity : AppCompatActivity() { showWelcomeDialog() } + RxBus.dismissNowPlayingCommandObservable.subscribe { + nowPlayingHidden = true + hideNowPlaying() + } + nowPlayingEventListener = object : NowPlayingEventListener { - override fun onDismissNowPlaying() { - nowPlayingHidden = true - hideNowPlaying() - } override fun onHideNowPlaying() { hideNowPlaying() @@ -184,12 +184,11 @@ class NavigationActivity : AppCompatActivity() { } } - themeChangedEventListener = object : ThemeChangedEventListener { - override fun onThemeChanged() { recreate() } + themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe { + recreate() } nowPlayingEventDistributor.subscribe(nowPlayingEventListener) - themeChangedEventDistributor.subscribe(themeChangedEventListener) serverRepository.liveServerCount().observe( this, @@ -238,7 +237,7 @@ class NavigationActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener) - themeChangedEventDistributor.unsubscribe(themeChangedEventListener) + themeChangedEventSubscription?.dispose() imageLoaderProvider.clearImageLoader() permissionUtil.onForegroundApplicationStopped() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt index e44e0774..f7996ca7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt @@ -8,7 +8,6 @@ import org.moire.ultrasonic.util.MediaSessionEventDistributor import org.moire.ultrasonic.util.MediaSessionHandler import org.moire.ultrasonic.util.NowPlayingEventDistributor import org.moire.ultrasonic.util.PermissionUtil -import org.moire.ultrasonic.util.ThemeChangedEventDistributor /** * This Koin module contains the registration of general classes needed for Ultrasonic @@ -18,7 +17,6 @@ val applicationModule = module { single { ImageLoaderProvider(androidContext()) } single { PermissionUtil(androidContext()) } single { NowPlayingEventDistributor() } - single { ThemeChangedEventDistributor() } single { MediaSessionEventDistributor() } single { MediaSessionHandler() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index b8f55481..60547765 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -14,6 +14,7 @@ import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.session.MediaSessionCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants +import io.reactivex.rxjava3.disposables.Disposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -93,16 +94,17 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { private val useId3Tags get() = Settings.shouldUseId3Tags private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId + private var mediaSessionTokenSubscription: Disposable? = null + @Suppress("MagicNumber") override fun onCreate() { super.onCreate() + mediaSessionTokenSubscription = RxBus.mediaSessionTokenObservable.subscribe { + if (sessionToken == null) sessionToken = it + } + mediaSessionEventListener = object : MediaSessionEventListener { - override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) { - if (sessionToken == null) { - sessionToken = token - } - } override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) { Timber.d( @@ -182,6 +184,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { override fun onDestroy() { super.onDestroy() + mediaSessionTokenSubscription?.dispose() mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener) mediaSessionHandler.release() serviceJob.cancel() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 1bcdfce5..6fd548c9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -14,6 +14,7 @@ import android.content.IntentFilter import android.media.AudioManager import android.os.Build import android.view.KeyEvent +import io.reactivex.rxjava3.disposables.Disposable import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R @@ -39,7 +40,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { private var created = false private var headsetEventReceiver: BroadcastReceiver? = null - private lateinit var mediaSessionEventListener: MediaSessionEventListener + private var mediaButtonEventSubscription: Disposable? = null fun onCreate() { onCreate(false, null) @@ -52,13 +53,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { return } - mediaSessionEventListener = object : MediaSessionEventListener { - override fun onMediaButtonEvent(keyEvent: KeyEvent?) { - if (keyEvent != null) handleKeyEvent(keyEvent) - } + mediaButtonEventSubscription = RxBus.mediaButtonEventObservable.subscribe { + handleKeyEvent(it) } - mediaSessionEventDistributor.subscribe(mediaSessionEventListener) registerHeadsetReceiver() mediaPlayerController.onCreate() if (autoPlay) mediaPlayerController.preload() @@ -98,9 +96,8 @@ class MediaPlayerLifecycleSupport : KoinComponent { mediaPlayerController.playerPosition ) - mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener) - mediaPlayerController.clear(false) + mediaButtonEventSubscription?.dispose() applicationContext().unregisterReceiver(headsetEventReceiver) mediaPlayerController.onDestroy() @@ -165,12 +162,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { } } - val headsetIntentFilter: IntentFilter = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - IntentFilter(AudioManager.ACTION_HEADSET_PLUG) - } else { - IntentFilter(Intent.ACTION_HEADSET_PLUG) - } + val headsetIntentFilter = IntentFilter(AudioManager.ACTION_HEADSET_PLUG) applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index 21a8e5f8..71402dac 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -22,6 +22,7 @@ import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import io.reactivex.rxjava3.disposables.Disposable import kotlin.collections.ArrayList import org.koin.android.ext.android.inject import org.moire.ultrasonic.R @@ -73,6 +74,7 @@ class MediaPlayerService : Service() { private var isInForeground = false private var notificationBuilder: NotificationCompat.Builder? = null private lateinit var mediaSessionEventListener: MediaSessionEventListener + private var mediaSessionTokenSubscription: Disposable? = null private val repeatMode: RepeatMode get() = Settings.repeatMode @@ -102,11 +104,11 @@ class MediaPlayerService : Service() { localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() } - mediaSessionEventListener = object : MediaSessionEventListener { - override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) { - mediaSessionToken = token - } + mediaSessionTokenSubscription = RxBus.mediaSessionTokenObservable.subscribe { + mediaSessionToken = it + } + mediaSessionEventListener = object : MediaSessionEventListener { override fun onSkipToQueueItemRequested(id: Long) { play(id.toInt()) } @@ -134,6 +136,7 @@ class MediaPlayerService : Service() { super.onDestroy() instance = null try { + mediaSessionTokenSubscription?.dispose() mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener) mediaSessionHandler.release() @@ -357,19 +360,11 @@ class MediaPlayerService : Service() { private fun setupOnCurrentPlayingChangedHandler() { localMediaPlayer.onCurrentPlayingChanged = { currentPlaying: DownloadFile? -> - if (currentPlaying != null) { - Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song) - Util.broadcastA2dpMetaDataChange( - this@MediaPlayerService, playerPosition, currentPlaying, - downloader.all.size, downloader.currentPlayingIndex + 1 - ) - } else { - Util.broadcastNewTrackInfo(this@MediaPlayerService, null) - Util.broadcastA2dpMetaDataChange( - this@MediaPlayerService, playerPosition, null, - downloader.all.size, downloader.currentPlayingIndex + 1 - ) - } + Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song) + Util.broadcastA2dpMetaDataChange( + this@MediaPlayerService, playerPosition, currentPlaying, + downloader.all.size, downloader.currentPlayingIndex + 1 + ) // Update widget val playerState = localMediaPlayer.playerState diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt new file mode 100644 index 00000000..f5ec3885 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -0,0 +1,42 @@ +package org.moire.ultrasonic.service + +import android.support.v4.media.session.MediaSessionCompat +import android.view.KeyEvent +import io.reactivex.rxjava3.subjects.PublishSubject +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.observables.ConnectableObservable +import timber.log.Timber + +object RxBus { + var mediaSessionTokenPublisher: PublishSubject = + PublishSubject.create() + val mediaSessionTokenObservable: Observable = + mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread()) + .replay(1) + .autoConnect() + .doOnEach { Timber.d("RxBus mediaSessionTokenPublisher onEach $it")} + + val mediaButtonEventPublisher: PublishSubject = + PublishSubject.create() + val mediaButtonEventObservable: Observable = + mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread()) + .doOnEach { Timber.d("RxBus mediaButtonEventPublisher onEach $it")} + + val themeChangedEventPublisher: PublishSubject = + PublishSubject.create() + val themeChangedEventObservable: Observable = + themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + .doOnEach { Timber.d("RxBus themeChangedEventPublisher onEach $it")} + + val dismissNowPlayingCommandPublisher: PublishSubject = + PublishSubject.create() + val dismissNowPlayingCommandObservable: Observable = + dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + .doOnEach { Timber.d("RxBus dismissNowPlayingCommandPublisher onEach $it")} + + fun releaseMediaSessionToken() { mediaSessionTokenPublisher = PublishSubject.create() } + +} + + diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt index a9ecade8..fc5b3ab6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt @@ -23,30 +23,12 @@ class MediaSessionEventDistributor { 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 -> @@ -61,8 +43,4 @@ class MediaSessionEventDistributor { fun raiseSkipToQueueItemRequestedEvent(id: Long) { eventListenerList.forEach { listener -> listener.onSkipToQueueItemRequested(id) } } - - fun raiseMediaButtonEvent(keyEvent: KeyEvent?) { - eventListenerList.forEach { listener -> listener.onMediaButtonEvent(keyEvent) } - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt index e4075248..fe59496b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt @@ -15,9 +15,9 @@ import android.view.KeyEvent * Callback interface for MediaSession related event subscribers */ interface MediaSessionEventListener { - fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {} +// 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?) {} +// fun onMediaButtonEvent(keyEvent: KeyEvent?) {} } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt index d7ebf424..a0db5943 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt @@ -25,6 +25,7 @@ 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 org.moire.ultrasonic.service.RxBus import timber.log.Timber private const val INTENT_CODE_MEDIA_BUTTON = 161 @@ -53,7 +54,7 @@ class MediaSessionHandler : KoinComponent { if (referenceCount > 0) return mediaSession?.isActive = false - mediaSessionEventDistributor.releaseCachedMediaSessionToken() + RxBus.releaseMediaSessionToken() mediaSession?.release() mediaSession = null @@ -72,7 +73,7 @@ class MediaSessionHandler : KoinComponent { mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService") val mediaSessionToken = mediaSession?.sessionToken ?: return - mediaSessionEventDistributor.raiseMediaSessionTokenCreatedEvent(mediaSessionToken) + RxBus.mediaSessionTokenPublisher.onNext(mediaSessionToken) updateMediaButtonReceiver() @@ -147,7 +148,7 @@ class MediaSessionHandler : KoinComponent { // 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) + event?.let { RxBus.mediaButtonEventPublisher.onNext(it) } return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt index 0d19903e..785bdfb1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt @@ -23,8 +23,4 @@ class NowPlayingEventDistributor { fun raiseHideNowPlayingEvent() { eventListenerList.forEach { listener -> listener.onHideNowPlaying() } } - - fun raiseNowPlayingDismissedEvent() { - eventListenerList.forEach { listener -> listener.onDismissNowPlaying() } - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt index 3f4bd75e..edcbcf2c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt @@ -4,7 +4,6 @@ package org.moire.ultrasonic.util * Callback interface for Now Playing event subscribers */ interface NowPlayingEventListener { - fun onDismissNowPlaying() fun onHideNowPlaying() fun onShowNowPlaying() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventDistributor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventDistributor.kt deleted file mode 100644 index bdce05ab..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventDistributor.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.moire.ultrasonic.util - -/** - * This class distributes Theme change related events to its subscribers. - * It is a primitive implementation of a pub-sub event bus - */ -class ThemeChangedEventDistributor { - var eventListenerList: MutableList = - listOf().toMutableList() - - fun subscribe(listener: ThemeChangedEventListener) { - eventListenerList.add(listener) - } - - fun unsubscribe(listener: ThemeChangedEventListener) { - eventListenerList.remove(listener) - } - - fun RaiseThemeChangedEvent() { - eventListenerList.forEach { listener -> listener.onThemeChanged() } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventListener.kt deleted file mode 100644 index 5656f1d4..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.moire.ultrasonic.util - -/** - * Callback interface for Theme change event subscribers - */ -interface ThemeChangedEventListener { - fun onThemeChanged() -} From ffb2d5988669ff015b5170c4afa8e3f8b83e45f2 Mon Sep 17 00:00:00 2001 From: Nite Date: Tue, 2 Nov 2021 17:45:01 +0100 Subject: [PATCH 2/7] Updated Events to ReactiveX Minor fixes --- .../fragment/NowPlayingFragment.java | 194 ----------------- .../ultrasonic/activity/NavigationActivity.kt | 21 +- .../moire/ultrasonic/di/ApplicationModule.kt | 4 - .../ultrasonic/fragment/NowPlayingFragment.kt | 188 ++++++++++++++++ .../service/AutoMediaBrowserService.kt | 135 ++++++------ .../moire/ultrasonic/service/Downloader.kt | 25 ++- .../ultrasonic/service/LocalMediaPlayer.kt | 67 +++--- .../service/MediaPlayerController.kt | 4 +- .../service/MediaPlayerLifecycleSupport.kt | 3 - .../ultrasonic/service/MediaPlayerService.kt | 204 ++++++++---------- .../service/PlaybackStateSerializer.kt | 8 +- .../org/moire/ultrasonic/service/RxBus.kt | 107 ++++++--- .../util/MediaSessionEventDistributor.kt | 46 ---- .../util/MediaSessionEventListener.kt | 23 -- .../ultrasonic/util/MediaSessionHandler.kt | 87 ++++---- .../util/NowPlayingEventDistributor.kt | 26 --- .../util/NowPlayingEventListener.kt | 9 - 17 files changed, 530 insertions(+), 621 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java deleted file mode 100644 index cd5246cd..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.MediaPlayerController; -import org.moire.ultrasonic.service.RxBus; -import org.moire.ultrasonic.subsonic.ImageLoaderProvider; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.NowPlayingEventDistributor; -import org.moire.ultrasonic.util.NowPlayingEventListener; -import org.moire.ultrasonic.util.Settings; -import org.moire.ultrasonic.util.Util; - -import kotlin.Lazy; -import kotlin.Unit; -import timber.log.Timber; - -import static org.koin.java.KoinJavaComponent.inject; - - -/** - * Contains the mini-now playing information box displayed at the bottom of the screen - */ -public class NowPlayingFragment extends Fragment { - - private static final int MIN_DISTANCE = 30; - private float downX; - private float downY; - ImageView playButton; - ImageView nowPlayingAlbumArtImage; - TextView nowPlayingTrack; - TextView nowPlayingArtist; - - private final Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); - private final Lazy imageLoader = inject(ImageLoaderProvider.class); - private final Lazy nowPlayingEventDistributor = inject(NowPlayingEventDistributor.class); - private NowPlayingEventListener nowPlayingEventListener; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - Util.applyTheme(this.getContext()); - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.now_playing, container, false); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) { - - playButton = view.findViewById(R.id.now_playing_control_play); - nowPlayingAlbumArtImage = view.findViewById(R.id.now_playing_image); - nowPlayingTrack = view.findViewById(R.id.now_playing_trackname); - nowPlayingArtist = view.findViewById(R.id.now_playing_artist); - - nowPlayingEventListener = new NowPlayingEventListener() { - @Override - public void onHideNowPlaying() { } - @Override - public void onShowNowPlaying() { update(); } - }; - - nowPlayingEventDistributor.getValue().subscribe(nowPlayingEventListener); - } - - @Override - public void onResume() { - super.onResume(); - update(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - nowPlayingEventDistributor.getValue().unsubscribe(nowPlayingEventListener); - } - - private void update() { - try - { - PlayerState playerState = mediaPlayerControllerLazy.getValue().getPlayerState(); - if (playerState == PlayerState.PAUSED) { - playButton.setImageDrawable(Util.getDrawableFromAttribute(getContext(), R.attr.media_play)); - } else if (playerState == PlayerState.STARTED) { - playButton.setImageDrawable(Util.getDrawableFromAttribute(getContext(), R.attr.media_pause)); - } - - DownloadFile file = mediaPlayerControllerLazy.getValue().getCurrentPlaying(); - if (file != null) { - final MusicDirectory.Entry song = file.getSong(); - String title = song.getTitle(); - String artist = song.getArtist(); - - imageLoader.getValue().getImageLoader().loadImage(nowPlayingAlbumArtImage, song, false, Util.getNotificationImageSize(getContext())); - nowPlayingTrack.setText(title); - nowPlayingArtist.setText(artist); - - nowPlayingAlbumArtImage.setOnClickListener(v -> { - Bundle bundle = new Bundle(); - - if (Settings.getShouldUseId3Tags()) { - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true); - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, song.getAlbumId()); - } else { - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false); - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, song.getParent()); - } - - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum()); - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum()); - Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(R.id.trackCollectionFragment, bundle); - }); - } - - getView().setOnTouchListener((v, event) -> handleOnTouch(event)); - - // This empty onClickListener is necessary for the onTouchListener to work - getView().setOnClickListener(v -> {}); - - playButton.setOnClickListener(v -> mediaPlayerControllerLazy.getValue().togglePlayPause()); - } - catch (Exception x) { - Timber.w(x, "Failed to get notification cover art"); - } - } - - private boolean handleOnTouch(MotionEvent event) { - switch (event.getAction()) - { - case MotionEvent.ACTION_DOWN: - { - downX = event.getX(); - downY = event.getY(); - return false; - } - case MotionEvent.ACTION_UP: - { - float upX = event.getX(); - float upY = event.getY(); - - float deltaX = downX - upX; - float deltaY = downY - upY; - - if (Math.abs(deltaX) > MIN_DISTANCE) - { - // left or right - if (deltaX < 0) - { - mediaPlayerControllerLazy.getValue().previous(); - return false; - } - if (deltaX > 0) - { - mediaPlayerControllerLazy.getValue().next(); - return false; - } - } - else if (Math.abs(deltaY) > MIN_DISTANCE) - { - if (deltaY < 0) - { - RxBus.INSTANCE.getDismissNowPlayingCommandPublisher().onNext(Unit.INSTANCE); - return false; - } - if (deltaY > 0) - { - return false; - } - } - Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(R.id.playerFragment); - return false; - } - } - return false; - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index a98b042c..62040423 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -48,8 +48,6 @@ import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil -import org.moire.ultrasonic.util.NowPlayingEventDistributor -import org.moire.ultrasonic.util.NowPlayingEventListener import org.moire.ultrasonic.util.PermissionUtil import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.Settings @@ -74,14 +72,13 @@ class NavigationActivity : AppCompatActivity() { private var headerBackgroundImage: ImageView? = null private lateinit var appBarConfiguration: AppBarConfiguration - private lateinit var nowPlayingEventListener: NowPlayingEventListener private var themeChangedEventSubscription: Disposable? = null + private var playerStateSubscription: Disposable? = null private val serverSettingsModel: ServerSettingsModel by viewModel() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() private val mediaPlayerController: MediaPlayerController by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() - private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject() private val permissionUtil: PermissionUtil by inject() private val activeServerProvider: ActiveServerProvider by inject() private val serverRepository: ServerSettingDao by inject() @@ -173,23 +170,17 @@ class NavigationActivity : AppCompatActivity() { hideNowPlaying() } - nowPlayingEventListener = object : NowPlayingEventListener { - - override fun onHideNowPlaying() { - hideNowPlaying() - } - - override fun onShowNowPlaying() { + playerStateSubscription = RxBus.playerStateObservable.subscribe { + if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED) showNowPlaying() - } + else + hideNowPlaying() } themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe { recreate() } - nowPlayingEventDistributor.subscribe(nowPlayingEventListener) - serverRepository.liveServerCount().observe( this, { count -> @@ -236,8 +227,8 @@ class NavigationActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() - nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener) themeChangedEventSubscription?.dispose() + playerStateSubscription?.dispose() imageLoaderProvider.clearImageLoader() permissionUtil.onForegroundApplicationStopped() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt index f7996ca7..cecdce6e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt @@ -4,9 +4,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import org.moire.ultrasonic.data.ActiveServerProvider 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.PermissionUtil /** @@ -16,7 +14,5 @@ val applicationModule = module { single { ActiveServerProvider(get()) } single { ImageLoaderProvider(androidContext()) } single { PermissionUtil(androidContext()) } - single { NowPlayingEventDistributor() } - single { MediaSessionEventDistributor() } single { MediaSessionHandler() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt new file mode 100644 index 00000000..93b62077 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -0,0 +1,188 @@ +/* + * NowPlayingFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.fragment + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.navigation.Navigation +import io.reactivex.rxjava3.disposables.Disposable +import java.lang.Exception +import kotlin.math.abs +import org.koin.android.ext.android.inject +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.RxBus +import org.moire.ultrasonic.subsonic.ImageLoaderProvider +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.util.Util.applyTheme +import org.moire.ultrasonic.util.Util.getDrawableFromAttribute +import org.moire.ultrasonic.util.Util.getNotificationImageSize +import timber.log.Timber + +/** + * Contains the mini-now playing information box displayed at the bottom of the screen + */ +class NowPlayingFragment : Fragment() { + + private var downX = 0f + private var downY = 0f + + private var playButton: ImageView? = null + private var nowPlayingAlbumArtImage: ImageView? = null + private var nowPlayingTrack: TextView? = null + private var nowPlayingArtist: TextView? = null + + private var playerStateSubscription: Disposable? = null + private val mediaPlayerController: MediaPlayerController by inject() + private val imageLoader: ImageLoaderProvider by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + applyTheme(this.context) + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.now_playing, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + playButton = view.findViewById(R.id.now_playing_control_play) + nowPlayingAlbumArtImage = view.findViewById(R.id.now_playing_image) + nowPlayingTrack = view.findViewById(R.id.now_playing_trackname) + nowPlayingArtist = view.findViewById(R.id.now_playing_artist) + playerStateSubscription = + RxBus.playerStateObservable.subscribe { update() } + } + + override fun onResume() { + super.onResume() + update() + } + + override fun onDestroy() { + super.onDestroy() + playerStateSubscription!!.dispose() + } + + @SuppressLint("ClickableViewAccessibility") + private fun update() { + try { + val playerState = mediaPlayerController.playerState + + if (playerState === PlayerState.PAUSED) { + playButton!!.setImageDrawable( + getDrawableFromAttribute( + context, R.attr.media_play + ) + ) + } else if (playerState === PlayerState.STARTED) { + playButton!!.setImageDrawable( + getDrawableFromAttribute( + context, R.attr.media_pause + ) + ) + } + + val file = mediaPlayerController.currentPlaying + + if (file != null) { + val song = file.song + val title = song.title + val artist = song.artist + + imageLoader.getImageLoader().loadImage( + nowPlayingAlbumArtImage, + song, + false, + getNotificationImageSize(requireContext()) + ) + + nowPlayingTrack!!.text = title + nowPlayingArtist!!.text = artist + + nowPlayingAlbumArtImage!!.setOnClickListener { + val bundle = Bundle() + + if (Settings.shouldUseId3Tags) { + bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true) + bundle.putString(Constants.INTENT_EXTRA_NAME_ID, song.albumId) + } else { + bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false) + bundle.putString(Constants.INTENT_EXTRA_NAME_ID, song.parent) + } + + bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.album) + bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.album) + + Navigation.findNavController(requireActivity(), R.id.nav_host_fragment) + .navigate(R.id.trackCollectionFragment, bundle) + } + } + requireView().setOnTouchListener { _: View?, event: MotionEvent -> + handleOnTouch(event) + } + + // This empty onClickListener is necessary for the onTouchListener to work + requireView().setOnClickListener { } + playButton!!.setOnClickListener { mediaPlayerController.togglePlayPause() } + } catch (all: Exception) { + Timber.w(all, "Failed to get notification cover art") + } + } + + private fun handleOnTouch(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + downX = event.x + downY = event.y + } + + MotionEvent.ACTION_UP -> { + val upX = event.x + val upY = event.y + val deltaX = downX - upX + val deltaY = downY - upY + + if (abs(deltaX) > MIN_DISTANCE) { + // left or right + if (deltaX < 0) { + mediaPlayerController.previous() + } + if (deltaX > 0) { + mediaPlayerController.next() + } + } else if (abs(deltaY) > MIN_DISTANCE) { + if (deltaY < 0) { + RxBus.dismissNowPlayingCommandPublisher.onNext(Unit) + } + } else { + Navigation.findNavController(requireActivity(), R.id.nav_host_fragment) + .navigate(R.id.playerFragment) + } + } + } + return false + } + + companion object { + private const val MIN_DISTANCE = 30 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index 60547765..173c5806 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -11,10 +11,9 @@ import android.os.Bundle import android.os.Handler import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat -import android.support.v4.media.session.MediaSessionCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants -import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -26,8 +25,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult -import org.moire.ultrasonic.util.MediaSessionEventDistributor -import org.moire.ultrasonic.util.MediaSessionEventListener import org.moire.ultrasonic.util.MediaSessionHandler import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util @@ -74,8 +71,6 @@ private const val SEARCH_LIMIT = 10 @Suppress("TooManyFunctions", "LargeClass") class AutoMediaBrowserService : MediaBrowserServiceCompat() { - private lateinit var mediaSessionEventListener: MediaSessionEventListener - private val mediaSessionEventDistributor by inject() private val lifecycleSupport by inject() private val mediaSessionHandler by inject() private val mediaPlayerController by inject() @@ -94,76 +89,24 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { private val useId3Tags get() = Settings.shouldUseId3Tags private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId - private var mediaSessionTokenSubscription: Disposable? = null + private var rxBusSubscription: CompositeDisposable = CompositeDisposable() @Suppress("MagicNumber") override fun onCreate() { super.onCreate() - mediaSessionTokenSubscription = RxBus.mediaSessionTokenObservable.subscribe { + rxBusSubscription += RxBus.mediaSessionTokenObservable.subscribe { if (sessionToken == null) sessionToken = it } - mediaSessionEventListener = object : MediaSessionEventListener { - - override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) { - Timber.d( - "AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s", - mediaId - ) - - if (mediaId == null) return - val mediaIdParts = mediaId.split('|') - - when (mediaIdParts.first()) { - MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2]) - MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong( - mediaIdParts[1], mediaIdParts[2], mediaIdParts[3] - ) - MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2]) - MEDIA_ALBUM_SONG_ITEM -> playAlbumSong( - mediaIdParts[1], mediaIdParts[2], mediaIdParts[3] - ) - MEDIA_SONG_STARRED_ID -> playStarredSongs() - MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1]) - MEDIA_SONG_RANDOM_ID -> playRandomSongs() - MEDIA_SONG_RANDOM_ITEM -> playRandomSong(mediaIdParts[1]) - MEDIA_SHARE_ITEM -> playShare(mediaIdParts[1]) - MEDIA_SHARE_SONG_ITEM -> playShareSong(mediaIdParts[1], mediaIdParts[2]) - MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1]) - MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1]) - MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode( - mediaIdParts[1], mediaIdParts[2] - ) - MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1]) - } - } - - override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) { - Timber.d("AutoMediaBrowserService onPlayFromSearchRequested query: %s", query) - if (query.isNullOrBlank()) playRandomSongs() - - serviceScope.launch { - val criteria = SearchCriteria(query!!, 0, 0, DISPLAY_LIMIT) - val searchResult = callWithErrorHandling { musicService.search(criteria) } - - // Try to find the best match - if (searchResult != null) { - val song = searchResult.songs - .asSequence() - .sortedByDescending { song -> song.starred } - .sortedByDescending { song -> song.averageRating } - .sortedByDescending { song -> song.userRating } - .sortedByDescending { song -> song.closeness } - .firstOrNull() - - if (song != null) playSong(song) - } - } - } + rxBusSubscription += RxBus.playFromMediaIdCommandObservable.subscribe { + playFromMediaId(it.first) + } + + rxBusSubscription += RxBus.playFromSearchCommandObservable.subscribe { + playFromSearchCommand(it.first) } - mediaSessionEventDistributor.subscribe(mediaSessionEventListener) mediaSessionHandler.initialize() val handler = Handler() @@ -182,10 +125,66 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { Timber.i("AutoMediaBrowserService onCreate finished") } + @Suppress("MagicNumber", "ComplexMethod") + private fun playFromMediaId(mediaId: String?) { + Timber.d( + "AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s", + mediaId + ) + + if (mediaId == null) return + val mediaIdParts = mediaId.split('|') + + when (mediaIdParts.first()) { + MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2]) + MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong( + mediaIdParts[1], mediaIdParts[2], mediaIdParts[3] + ) + MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2]) + MEDIA_ALBUM_SONG_ITEM -> playAlbumSong( + mediaIdParts[1], mediaIdParts[2], mediaIdParts[3] + ) + MEDIA_SONG_STARRED_ID -> playStarredSongs() + MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1]) + MEDIA_SONG_RANDOM_ID -> playRandomSongs() + MEDIA_SONG_RANDOM_ITEM -> playRandomSong(mediaIdParts[1]) + MEDIA_SHARE_ITEM -> playShare(mediaIdParts[1]) + MEDIA_SHARE_SONG_ITEM -> playShareSong(mediaIdParts[1], mediaIdParts[2]) + MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1]) + MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1]) + MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode( + mediaIdParts[1], mediaIdParts[2] + ) + MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1]) + } + } + + private fun playFromSearchCommand(query: String?) { + Timber.d("AutoMediaBrowserService onPlayFromSearchRequested query: %s", query) + if (query.isNullOrBlank()) playRandomSongs() + + serviceScope.launch { + val criteria = SearchCriteria(query!!, 0, 0, DISPLAY_LIMIT) + val searchResult = callWithErrorHandling { musicService.search(criteria) } + + // Try to find the best match + if (searchResult != null) { + val song = searchResult.songs + .asSequence() + .sortedByDescending { song -> song.starred } + .sortedByDescending { song -> song.averageRating } + .sortedByDescending { song -> song.userRating } + .sortedByDescending { song -> song.closeness } + .firstOrNull() + + if (song != null) playSong(song) + } + } + } + override fun onDestroy() { super.onDestroy() - mediaSessionTokenSubscription?.dispose() - mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener) + rxBusSubscription.dispose() mediaSessionHandler.release() serviceJob.cancel() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 3f27c85d..a276f1e2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -31,6 +31,7 @@ class Downloader( private val localMediaPlayer: LocalMediaPlayer ) : KoinComponent { val playlist: MutableList = ArrayList() + var started: Boolean = false private val downloadQueue: PriorityQueue = PriorityQueue() @@ -46,7 +47,10 @@ class Downloader( private var wifiLock: WifiManager.WifiLock? = null var playlistUpdateRevision: Long = 0 - private set + private set(value) { + field = value + RxBus.playlistPublisher.onNext(playlist) + } val downloadChecker = Runnable { try { @@ -349,6 +353,20 @@ class Downloader( checkDownloads() } + @Synchronized + fun clearIncomplete() { + val iterator = playlist.iterator() + var changedPlaylist = false + while (iterator.hasNext()) { + val downloadFile = iterator.next() + if (!downloadFile.isCompleteFileAvailable) { + iterator.remove() + changedPlaylist = true + } + } + if (changedPlaylist) playlistUpdateRevision++ + } + @Synchronized fun downloadBackground(songs: List, save: Boolean) { @@ -429,18 +447,21 @@ class Downloader( playlistUpdateRevision++ } } + if (revisionBefore != playlistUpdateRevision) { jukeboxMediaPlayer.updatePlaylist() } + if (wasEmpty && playlist.isNotEmpty()) { if (jukeboxMediaPlayer.isEnabled) { jukeboxMediaPlayer.skip(0, 0) - localMediaPlayer.setPlayerState(PlayerState.STARTED) + localMediaPlayer.setPlayerState(PlayerState.STARTED, playlist[0]) } else { localMediaPlayer.play(playlist[0]) } } } + companion object { const val PARALLEL_DOWNLOADS = 3 const val CHECK_INTERVAL = 5L diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 0b0a1b42..85217389 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -32,7 +32,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.MediaSessionHandler import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.StreamProxy import org.moire.ultrasonic.util.Util @@ -46,17 +45,10 @@ class LocalMediaPlayer : KoinComponent { private val audioFocusHandler by inject() private val context by inject() - private val mediaSessionHandler by inject() - - @JvmField - var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null @JvmField var onSongCompleted: ((DownloadFile?) -> Unit?)? = null - @JvmField - var onPlayerStateChanged: ((PlayerState, DownloadFile?) -> Unit?)? = null - @JvmField var onPrepared: (() -> Any?)? = null @@ -64,6 +56,7 @@ class LocalMediaPlayer : KoinComponent { var onNextSongRequested: Runnable? = null @JvmField + @Volatile var playerState = PlayerState.IDLE @JvmField @@ -132,7 +125,6 @@ class LocalMediaPlayer : KoinComponent { // 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() try { val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) @@ -164,21 +156,17 @@ class LocalMediaPlayer : KoinComponent { } @Synchronized - fun setPlayerState(playerState: PlayerState) { - Timber.i("%s -> %s (%s)", this.playerState.name, playerState.name, currentPlaying) - this.playerState = playerState + fun setPlayerState(playerState: PlayerState, track: DownloadFile?) { + Timber.i("%s -> %s (%s)", this.playerState.name, playerState.name, track) + synchronized(playerState) { + this.playerState = playerState + } if (playerState === PlayerState.STARTED) { audioFocusHandler.requestAudioFocus() } - if (onPlayerStateChanged != null) { - val mainHandler = Handler(context.mainLooper) + RxBus.playerStatePublisher.onNext(RxBus.StateWithTrack(playerState, track)) - val myRunnable = Runnable { - onPlayerStateChanged?.invoke(playerState, currentPlaying) - } - mainHandler.post(myRunnable) - } if (playerState === PlayerState.STARTED && positionCache == null) { positionCache = PositionCache() val thread = Thread(positionCache) @@ -194,14 +182,10 @@ class LocalMediaPlayer : KoinComponent { */ @Synchronized fun setCurrentPlaying(currentPlaying: DownloadFile?) { - Timber.v("setCurrentPlaying %s", currentPlaying) + // In some cases this function is called twice + if (this.currentPlaying == currentPlaying) return this.currentPlaying = currentPlaying - - if (onCurrentPlayingChanged != null) { - val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { onCurrentPlayingChanged!!(currentPlaying) } - mainHandler.post(myRunnable) - } + RxBus.currentPlayingPublisher.onNext(RxBus.StateWithTrack(playerState, currentPlaying)) } /* @@ -262,7 +246,7 @@ class LocalMediaPlayer : KoinComponent { mediaPlayer = nextMediaPlayer!! setCurrentPlaying(nextPlaying) - setPlayerState(PlayerState.STARTED) + setPlayerState(PlayerState.STARTED, currentPlaying) attachHandlersToPlayer(mediaPlayer, nextPlaying!!, false) @@ -343,7 +327,7 @@ class LocalMediaPlayer : KoinComponent { @Synchronized private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) { - if (playerState !== PlayerState.PREPARED) { + if (playerState !== PlayerState.PREPARED && !fileToPlay.isWorkDone) { reset() bufferTask = BufferTask(fileToPlay, position, autoStart) bufferTask!!.start() @@ -354,6 +338,7 @@ class LocalMediaPlayer : KoinComponent { @Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) { + setPlayerState(PlayerState.IDLE, downloadFile) // In many cases we will be resetting the mediaPlayer a second time here. // figure out if we can remove this call... @@ -368,7 +353,6 @@ class LocalMediaPlayer : KoinComponent { downloadFile.updateModificationDate() mediaPlayer.setOnCompletionListener(null) - setPlayerState(PlayerState.IDLE) setAudioAttributes(mediaPlayer) var dataSource = file.path @@ -394,7 +378,7 @@ class LocalMediaPlayer : KoinComponent { Timber.i("Preparing media player") mediaPlayer.setDataSource(dataSource) - setPlayerState(PlayerState.PREPARING) + setPlayerState(PlayerState.PREPARING, downloadFile) mediaPlayer.setOnBufferingUpdateListener { mp, percent -> val song = downloadFile.song @@ -412,7 +396,7 @@ class LocalMediaPlayer : KoinComponent { mediaPlayer.setOnPreparedListener { Timber.i("Media player prepared") - setPlayerState(PlayerState.PREPARED) + setPlayerState(PlayerState.PREPARED, downloadFile) // Populate seek bar secondary progress if we have a complete file for consistency if (downloadFile.isWorkDone) { @@ -427,9 +411,9 @@ class LocalMediaPlayer : KoinComponent { cachedPosition = position if (start) { mediaPlayer.start() - setPlayerState(PlayerState.STARTED) + setPlayerState(PlayerState.STARTED, downloadFile) } else { - setPlayerState(PlayerState.PAUSED) + setPlayerState(PlayerState.PAUSED, downloadFile) } } @@ -437,6 +421,7 @@ class LocalMediaPlayer : KoinComponent { onPrepared } } + attachHandlersToPlayer(mediaPlayer, downloadFile, partial) mediaPlayer.prepareAsync() } catch (x: Exception) { @@ -527,7 +512,7 @@ class LocalMediaPlayer : KoinComponent { Timber.i("Ending position %d of %d", pos, duration) if (!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000) { - setPlayerState(PlayerState.COMPLETED) + setPlayerState(PlayerState.COMPLETED, downloadFile) if (Settings.gaplessPlayback && nextPlaying != null && nextPlayerState === PlayerState.PREPARED @@ -574,7 +559,7 @@ class LocalMediaPlayer : KoinComponent { resetMediaPlayer() try { - setPlayerState(PlayerState.IDLE) + setPlayerState(PlayerState.IDLE, currentPlaying) mediaPlayer.setOnErrorListener(null) mediaPlayer.setOnCompletionListener(null) } catch (x: Exception) { @@ -603,7 +588,7 @@ class LocalMediaPlayer : KoinComponent { private val partialFile: File = downloadFile.partialFile override fun execute() { - setPlayerState(PlayerState.DOWNLOADING) + setPlayerState(PlayerState.DOWNLOADING, downloadFile) while (!bufferComplete() && !isOffline()) { Util.sleepQuietly(1000L) if (isCancelled) { @@ -702,10 +687,12 @@ class LocalMediaPlayer : KoinComponent { while (isRunning) { try { if (playerState === PlayerState.STARTED) { - cachedPosition = mediaPlayer.currentPosition - mediaSessionHandler.updateMediaSessionPlaybackPosition( - cachedPosition.toLong() - ) + synchronized(playerState) { + if (playerState === PlayerState.STARTED) { + cachedPosition = mediaPlayer.currentPosition + } + } + RxBus.playbackPositionPublisher.onNext(cachedPosition) } Util.sleepQuietly(100L) } catch (e: Exception) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 0e13666c..0dcb7fd9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -401,7 +401,8 @@ class MediaPlayerController( get() = localMediaPlayer.playerState set(state) { val mediaPlayerService = runningInstance - if (mediaPlayerService != null) localMediaPlayer.setPlayerState(state) + if (mediaPlayerService != null) + localMediaPlayer.setPlayerState(state, localMediaPlayer.currentPlaying) } @set:Synchronized @@ -483,6 +484,7 @@ class MediaPlayerController( Timber.e(e) } }.start() + // TODO this would be better handled with a Rx command updateNotification() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 7167c996..f48ec305 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -21,8 +21,6 @@ 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.Settings import timber.log.Timber @@ -35,7 +33,6 @@ class MediaPlayerLifecycleSupport : KoinComponent { private val playbackStateSerializer by inject() private val mediaPlayerController by inject() private val downloader by inject() - private val mediaSessionEventDistributor by inject() private var created = false private var headsetEventReceiver: BroadcastReceiver? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index 71402dac..522185f1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -22,7 +22,7 @@ import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlin.collections.ArrayList import org.koin.android.ext.android.inject import org.moire.ultrasonic.R @@ -38,10 +38,7 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3 import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4 import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService 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.Settings import org.moire.ultrasonic.util.ShufflePlayBuffer import org.moire.ultrasonic.util.SimpleServiceBinder @@ -65,19 +62,13 @@ class MediaPlayerService : Service() { private val shufflePlayBuffer by inject() private val downloader by inject() private val localMediaPlayer by inject() - private val nowPlayingEventDistributor by inject() - private val mediaSessionEventDistributor by inject() private val mediaSessionHandler by inject() private var mediaSession: MediaSessionCompat? = null private var mediaSessionToken: MediaSessionCompat.Token? = null private var isInForeground = false private var notificationBuilder: NotificationCompat.Builder? = null - private lateinit var mediaSessionEventListener: MediaSessionEventListener - private var mediaSessionTokenSubscription: Disposable? = null - - private val repeatMode: RepeatMode - get() = Settings.repeatMode + private var rxBusSubscription: CompositeDisposable = CompositeDisposable() override fun onBind(intent: Intent): IBinder { return binder @@ -89,8 +80,6 @@ class MediaPlayerService : Service() { shufflePlayBuffer.onCreate() localMediaPlayer.init() - setupOnCurrentPlayingChangedHandler() - setupOnPlayerStateChangedHandler() setupOnSongCompletedHandler() localMediaPlayer.onPrepared = { @@ -104,25 +93,32 @@ class MediaPlayerService : Service() { localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() } - mediaSessionTokenSubscription = RxBus.mediaSessionTokenObservable.subscribe { - mediaSessionToken = it - } - - mediaSessionEventListener = object : MediaSessionEventListener { - override fun onSkipToQueueItemRequested(id: Long) { - play(id.toInt()) - } - } - - mediaSessionEventDistributor.subscribe(mediaSessionEventListener) - mediaSessionHandler.initialize() - // Create Notification Channel createNotificationChannel() // Update notification early. It is better to show an empty one temporarily // than waiting too long and letting Android kill the app updateNotification(PlayerState.IDLE, null) + + // Subscribing should be after updateNotification to avoid concurrency + rxBusSubscription += RxBus.playerStateObservable.subscribe { + playerStateChangedHandler(it.state, it.track) + } + + rxBusSubscription += RxBus.currentPlayingObservable.subscribe { + currentPlayingChangedHandler(it.state, it.track) + } + + rxBusSubscription += RxBus.mediaSessionTokenObservable.subscribe { + mediaSessionToken = it + } + + rxBusSubscription += RxBus.skipToQueueItemCommandObservable.subscribe { + play(it.toInt()) + } + + mediaSessionHandler.initialize() + instance = this Timber.i("MediaPlayerService created") } @@ -136,9 +132,8 @@ class MediaPlayerService : Service() { super.onDestroy() instance = null try { - mediaSessionTokenSubscription?.dispose() - mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener) mediaSessionHandler.release() + rxBusSubscription.dispose() localMediaPlayer.release() downloader.stop() @@ -211,9 +206,7 @@ class MediaPlayerService : Service() { @Synchronized fun setNextPlaying() { - val gaplessPlayback = Settings.gaplessPlayback - - if (!gaplessPlayback) { + if (!Settings.gaplessPlayback) { localMediaPlayer.clearNextPlaying(true) return } @@ -221,7 +214,7 @@ class MediaPlayerService : Service() { var index = downloader.currentPlayingIndex if (index != -1) { - when (repeatMode) { + when (Settings.repeatMode) { RepeatMode.OFF -> index += 1 RepeatMode.ALL -> index = (index + 1) % downloader.playlist.size RepeatMode.SINGLE -> { @@ -293,7 +286,6 @@ class MediaPlayerService : Service() { if (start) { if (jukeboxMediaPlayer.isEnabled) { jukeboxMediaPlayer.skip(index, 0) - localMediaPlayer.setPlayerState(PlayerState.STARTED) } else { localMediaPlayer.play(downloader.playlist[index]) } @@ -321,7 +313,7 @@ class MediaPlayerService : Service() { } else { localMediaPlayer.pause() } - localMediaPlayer.setPlayerState(PlayerState.PAUSED) + localMediaPlayer.setPlayerState(PlayerState.PAUSED, localMediaPlayer.currentPlaying) } } @@ -334,7 +326,7 @@ class MediaPlayerService : Service() { localMediaPlayer.pause() } } - localMediaPlayer.setPlayerState(PlayerState.STOPPED) + localMediaPlayer.setPlayerState(PlayerState.STOPPED, null) } @Synchronized @@ -344,7 +336,7 @@ class MediaPlayerService : Service() { } else { localMediaPlayer.start() } - localMediaPlayer.setPlayerState(PlayerState.STARTED) + localMediaPlayer.setPlayerState(PlayerState.STARTED, localMediaPlayer.currentPlaying) } private fun updateWidget(playerState: PlayerState, song: MusicDirectory.Entry?) { @@ -357,92 +349,78 @@ class MediaPlayerService : Service() { UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false) } - private fun setupOnCurrentPlayingChangedHandler() { - localMediaPlayer.onCurrentPlayingChanged = { currentPlaying: DownloadFile? -> + private fun currentPlayingChangedHandler( + playerState: PlayerState, + currentPlaying: DownloadFile? + ) { + Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song) + Util.broadcastA2dpMetaDataChange( + this@MediaPlayerService, playerPosition, currentPlaying, + downloader.all.size, downloader.currentPlayingIndex + 1 + ) - Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song) - Util.broadcastA2dpMetaDataChange( - this@MediaPlayerService, playerPosition, currentPlaying, - downloader.all.size, downloader.currentPlayingIndex + 1 - ) + // Update widget + val song = currentPlaying?.song - // Update widget - val playerState = localMediaPlayer.playerState - val song = currentPlaying?.song + updateWidget(playerState, song) - updateWidget(playerState, song) - - if (currentPlaying != null) { - updateNotification(localMediaPlayer.playerState, currentPlaying) - nowPlayingEventDistributor.raiseShowNowPlayingEvent() - } else { - nowPlayingEventDistributor.raiseHideNowPlayingEvent() - stopForeground(true) - isInForeground = false - stopIfIdle() - } - null + if (currentPlaying != null) { + updateNotification(playerState, currentPlaying) + } else { + stopForeground(true) + isInForeground = false + stopIfIdle() } + + Timber.d("Processed currently playing track change") } - private fun setupOnPlayerStateChangedHandler() { - localMediaPlayer.onPlayerStateChanged = { - playerState: PlayerState, - currentPlaying: DownloadFile? - -> + private fun playerStateChangedHandler( + playerState: PlayerState, + currentPlaying: DownloadFile? + ) { - val context = this@MediaPlayerService + val context = this@MediaPlayerService - // Notify MediaSession - mediaSessionHandler.updateMediaSession( - currentPlaying, - downloader.currentPlayingIndex.toLong(), - playerState + if (playerState === PlayerState.PAUSED) { + playbackStateSerializer.serialize( + downloader.playlist, downloader.currentPlayingIndex, playerPosition ) - - if (playerState === PlayerState.PAUSED) { - playbackStateSerializer.serialize( - downloader.playlist, downloader.currentPlayingIndex, playerPosition - ) - } - - val showWhenPaused = playerState !== PlayerState.STOPPED && - Settings.isNotificationAlwaysEnabled - - val show = playerState === PlayerState.STARTED || showWhenPaused - val song = currentPlaying?.song - - Util.broadcastPlaybackStatusChange(context, playerState) - Util.broadcastA2dpPlayStatusChange( - context, playerState, song, - downloader.playlist.size, - downloader.playlist.indexOf(currentPlaying) + 1, playerPosition - ) - - // Update widget - updateWidget(playerState, song) - - if (show) { - // Only update notification if player state is one that will change the icon - if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) { - updateNotification(playerState, currentPlaying) - nowPlayingEventDistributor.raiseShowNowPlayingEvent() - } - } else { - nowPlayingEventDistributor.raiseHideNowPlayingEvent() - stopForeground(true) - isInForeground = false - stopIfIdle() - } - - if (playerState === PlayerState.STARTED) { - scrobbler.scrobble(currentPlaying, false) - } else if (playerState === PlayerState.COMPLETED) { - scrobbler.scrobble(currentPlaying, true) - } - - null } + + val showWhenPaused = playerState !== PlayerState.STOPPED && + Settings.isNotificationAlwaysEnabled + + val show = playerState === PlayerState.STARTED || showWhenPaused + val song = currentPlaying?.song + + Util.broadcastPlaybackStatusChange(context, playerState) + Util.broadcastA2dpPlayStatusChange( + context, playerState, song, + downloader.playlist.size, + downloader.playlist.indexOf(currentPlaying) + 1, playerPosition + ) + + // Update widget + updateWidget(playerState, song) + + if (show) { + // Only update notification if player state is one that will change the icon + if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) { + updateNotification(playerState, currentPlaying) + } + } else { + stopForeground(true) + isInForeground = false + stopIfIdle() + } + + if (playerState === PlayerState.STARTED) { + scrobbler.scrobble(currentPlaying, false) + } else if (playerState === PlayerState.COMPLETED) { + scrobbler.scrobble(currentPlaying, true) + } + Timber.d("Processed player state change") } private fun setupOnSongCompletedHandler() { @@ -460,7 +438,7 @@ class MediaPlayerService : Service() { } } if (index != -1) { - when (repeatMode) { + when (Settings.repeatMode) { RepeatMode.OFF -> { if (index + 1 < 0 || index + 1 >= downloader.playlist.size) { if (Settings.shouldClearPlaylist) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index 4e3c0692..df9e4390 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -19,7 +19,6 @@ 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 /** @@ -30,9 +29,8 @@ import timber.log.Timber class PlaybackStateSerializer : KoinComponent { private val context by inject() - private val mediaSessionHandler by inject() - val lock: Lock = ReentrantLock() + private val lock: Lock = ReentrantLock() private val setup = AtomicBoolean(false) private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -76,9 +74,6 @@ class PlaybackStateSerializer : KoinComponent { ) FileUtil.serialize(context, state, Constants.FILENAME_PLAYLIST_SER) - - // This is called here because the queue is usually serialized after a change - mediaSessionHandler.updateMediaSessionQueue(state.songs) } fun deserialize(afterDeserialized: (State?) -> Unit?) { @@ -106,7 +101,6 @@ class PlaybackStateSerializer : KoinComponent { state.currentPlayingPosition ) - mediaSessionHandler.updateMediaSessionQueue(state.songs) afterDeserialized(state) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index f5ec3885..fca60614 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -1,42 +1,93 @@ package org.moire.ultrasonic.service +import android.os.Bundle import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent -import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.observables.ConnectableObservable -import timber.log.Timber +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.subjects.PublishSubject +import java.util.concurrent.TimeUnit +import org.moire.ultrasonic.domain.PlayerState -object RxBus { - var mediaSessionTokenPublisher: PublishSubject = - PublishSubject.create() - val mediaSessionTokenObservable: Observable = - mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread()) - .replay(1) - .autoConnect() - .doOnEach { Timber.d("RxBus mediaSessionTokenPublisher onEach $it")} +class RxBus { + companion object { + var mediaSessionTokenPublisher: PublishSubject = + PublishSubject.create() + val mediaSessionTokenObservable: Observable = + mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread()) + .replay(1) + .autoConnect(0) - val mediaButtonEventPublisher: PublishSubject = - PublishSubject.create() - val mediaButtonEventObservable: Observable = - mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread()) - .doOnEach { Timber.d("RxBus mediaButtonEventPublisher onEach $it")} + val mediaButtonEventPublisher: PublishSubject = + PublishSubject.create() + val mediaButtonEventObservable: Observable = + mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread()) - val themeChangedEventPublisher: PublishSubject = - PublishSubject.create() - val themeChangedEventObservable: Observable = - themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) - .doOnEach { Timber.d("RxBus themeChangedEventPublisher onEach $it")} + val themeChangedEventPublisher: PublishSubject = + PublishSubject.create() + val themeChangedEventObservable: Observable = + themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) - val dismissNowPlayingCommandPublisher: PublishSubject = - PublishSubject.create() - val dismissNowPlayingCommandObservable: Observable = - dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) - .doOnEach { Timber.d("RxBus dismissNowPlayingCommandPublisher onEach $it")} + val playerStatePublisher: PublishSubject = + PublishSubject.create() + val playerStateObservable: Observable = + playerStatePublisher.observeOn(AndroidSchedulers.mainThread()) + .replay(1) + .autoConnect(0) - fun releaseMediaSessionToken() { mediaSessionTokenPublisher = PublishSubject.create() } + val currentPlayingPublisher: PublishSubject = + PublishSubject.create() + val currentPlayingObservable: Observable = + currentPlayingPublisher.observeOn(AndroidSchedulers.mainThread()) + .replay(1) + .autoConnect(0) + val playlistPublisher: PublishSubject> = + PublishSubject.create() + val playlistObservable: Observable> = + playlistPublisher.observeOn(AndroidSchedulers.mainThread()) + .replay(1) + .autoConnect(0) + + val playbackPositionPublisher: PublishSubject = + PublishSubject.create() + val playbackPositionObservable: Observable = + playbackPositionPublisher.observeOn(AndroidSchedulers.mainThread()) + .throttleFirst(1, TimeUnit.SECONDS) + .replay(1) + .autoConnect(0) + + // Commands + val dismissNowPlayingCommandPublisher: PublishSubject = + PublishSubject.create() + val dismissNowPlayingCommandObservable: Observable = + dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + + val playFromMediaIdCommandPublisher: PublishSubject> = + PublishSubject.create() + val playFromMediaIdCommandObservable: Observable> = + playFromMediaIdCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + + val playFromSearchCommandPublisher: PublishSubject> = + PublishSubject.create() + val playFromSearchCommandObservable: Observable> = + playFromSearchCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + + val skipToQueueItemCommandPublisher: PublishSubject = + PublishSubject.create() + val skipToQueueItemCommandObservable: Observable = + skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + + fun releaseMediaSessionToken() { + mediaSessionTokenPublisher = PublishSubject.create() + } + } + + data class StateWithTrack(val state: PlayerState, val track: DownloadFile?) } - +operator fun CompositeDisposable.plusAssign(disposable: Disposable) { + this.add(disposable) +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt deleted file mode 100644 index fc5b3ab6..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 = - listOf().toMutableList() - - var cachedToken: MediaSessionCompat.Token? = null - - fun subscribe(listener: MediaSessionEventListener) { - eventListenerList.add(listener) - } - - fun unsubscribe(listener: MediaSessionEventListener) { - eventListenerList.remove(listener) - } - - 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) } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt deleted file mode 100644 index fe59496b..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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?) {} -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt index a0db5943..4768f6eb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt @@ -17,19 +17,20 @@ 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 io.reactivex.rxjava3.disposables.CompositeDisposable +import kotlin.Pair 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 org.moire.ultrasonic.service.RxBus +import org.moire.ultrasonic.service.plusAssign 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 */ @@ -40,14 +41,14 @@ class MediaSessionHandler : KoinComponent { private var playbackActions: Long? = null private var cachedPlayingIndex: Long? = null - private val mediaSessionEventDistributor by inject() private val applicationContext by inject() private var referenceCount: Int = 0 - private var cachedPlaylist: List? = null - private var playbackPositionDelayCount: Int = 0 + private var cachedPlaylist: List? = null private var cachedPosition: Long = 0 + private val rxBusSubscription: CompositeDisposable = CompositeDisposable() + fun release() { if (referenceCount > 0) referenceCount-- @@ -55,6 +56,7 @@ class MediaSessionHandler : KoinComponent { mediaSession?.isActive = false RxBus.releaseMediaSessionToken() + rxBusSubscription.dispose() mediaSession?.release() mediaSession = null @@ -94,14 +96,14 @@ class MediaSessionHandler : KoinComponent { super.onPlayFromMediaId(mediaId, extras) Timber.d("Media Session Callback: onPlayFromMediaId %s", mediaId) - mediaSessionEventDistributor.raisePlayFromMediaIdRequestedEvent(mediaId, extras) + RxBus.playFromMediaIdCommandPublisher.onNext(Pair(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) + RxBus.playFromSearchCommandPublisher.onNext(Pair(query, extras)) } override fun onPause() { @@ -154,22 +156,30 @@ class MediaSessionHandler : KoinComponent { override fun onSkipToQueueItem(id: Long) { super.onSkipToQueueItem(id) - mediaSessionEventDistributor.raiseSkipToQueueItemRequestedEvent(id) + RxBus.skipToQueueItemCommandPublisher.onNext(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) setMediaSessionQueue(cachedPlaylist) + rxBusSubscription += RxBus.playbackPositionObservable.subscribe { + updateMediaSessionPlaybackPosition(it) + } + rxBusSubscription += RxBus.playlistObservable.subscribe { + updateMediaSessionQueue(it) + } + rxBusSubscription += RxBus.playerStateObservable.subscribe { + updateMediaSession(it.state, it.track) + } + Timber.i("MediaSessionHandler.initialize Media Session created") } - @Suppress("TooGenericExceptionCaught", "LongMethod") - fun updateMediaSession( - currentPlaying: DownloadFile?, - currentPlayingIndex: Long?, - playerState: PlayerState + @Suppress("LongMethod", "ComplexMethod") + private fun updateMediaSession( + playerState: PlayerState, + currentPlaying: DownloadFile? ) { Timber.d("Updating the MediaSession") @@ -188,8 +198,8 @@ class MediaSessionHandler : KoinComponent { 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") + } catch (all: Exception) { + Timber.e(all, "Error setting the metadata") } } @@ -245,52 +255,45 @@ class MediaSessionHandler : KoinComponent { // Set actions playbackStateBuilder.setActions(playbackActions!!) - cachedPlayingIndex = currentPlayingIndex - setMediaSessionQueue(cachedPlaylist) + val index = cachedPlaylist?.indexOf(currentPlaying) + cachedPlayingIndex = if (index == null || index < 0) null else index.toLong() + cachedPlaylist?.let { setMediaSessionQueue(it) } + if ( - currentPlayingIndex != null && cachedPlaylist != null && + cachedPlayingIndex != null && cachedPlaylist != null && !Settings.shouldDisableNowPlayingListSending ) - playbackStateBuilder.setActiveQueueItemId(currentPlayingIndex) + cachedPlayingIndex?.let { playbackStateBuilder.setActiveQueueItemId(it) } // Save the playback state mediaSession?.setPlaybackState(playbackStateBuilder.build()) } - fun updateMediaSessionQueue(playlist: Iterable) { - // This call is cached because Downloader may initialize earlier than the MediaSession - cachedPlaylist = playlist.mapIndexed { id, song -> - MediaSessionCompat.QueueItem( - Util.getMediaDescriptionForEntry(song), - id.toLong() - ) - } - setMediaSessionQueue(cachedPlaylist) + private fun updateMediaSessionQueue(playlist: List) { + cachedPlaylist = playlist + setMediaSessionQueue(playlist) } - private fun setMediaSessionQueue(queue: List?) { + private fun setMediaSessionQueue(playlist: List) { if (mediaSession == null) return if (Settings.shouldDisableNowPlayingListSending) return + val queue = playlist.mapIndexed { id, file -> + MediaSessionCompat.QueueItem( + Util.getMediaDescriptionForEntry(file.song), + id.toLong() + ) + } mediaSession?.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing)) mediaSession?.setQueue(queue) } - fun updateMediaSessionPlaybackPosition(playbackPosition: Long) { - - cachedPosition = playbackPosition - if (mediaSession == null) return - + private fun updateMediaSessionPlaybackPosition(playbackPosition: Int) { + cachedPosition = playbackPosition.toLong() 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.setState(playbackState!!, cachedPosition, 1.0f) playbackStateBuilder.setActions(playbackActions!!) if ( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt deleted file mode 100644 index 785bdfb1..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.moire.ultrasonic.util - -/** - * This class distributes Now Playing related events to its subscribers. - * It is a primitive implementation of a pub-sub event bus - */ -class NowPlayingEventDistributor { - private var eventListenerList: MutableList = - listOf().toMutableList() - - fun subscribe(listener: NowPlayingEventListener) { - eventListenerList.add(listener) - } - - fun unsubscribe(listener: NowPlayingEventListener) { - eventListenerList.remove(listener) - } - - fun raiseShowNowPlayingEvent() { - eventListenerList.forEach { listener -> listener.onShowNowPlaying() } - } - - fun raiseHideNowPlayingEvent() { - eventListenerList.forEach { listener -> listener.onHideNowPlaying() } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt deleted file mode 100644 index edcbcf2c..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.moire.ultrasonic.util - -/** - * Callback interface for Now Playing event subscribers - */ -interface NowPlayingEventListener { - fun onHideNowPlaying() - fun onShowNowPlaying() -} From 69825b28bbed5bd5689c6b418d25233a07e57fe0 Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 8 Nov 2021 20:38:05 +0100 Subject: [PATCH 3/7] Merged player state and track changes into a single event --- .../ultrasonic/service/LocalMediaPlayer.kt | 2 +- .../ultrasonic/service/MediaPlayerService.kt | 90 +++++++++---------- .../org/moire/ultrasonic/service/RxBus.kt | 7 -- 3 files changed, 43 insertions(+), 56 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 85217389..f63d7aea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -185,7 +185,7 @@ class LocalMediaPlayer : KoinComponent { // In some cases this function is called twice if (this.currentPlaying == currentPlaying) return this.currentPlaying = currentPlaying - RxBus.currentPlayingPublisher.onNext(RxBus.StateWithTrack(playerState, currentPlaying)) + RxBus.playerStatePublisher.onNext(RxBus.StateWithTrack(playerState, currentPlaying)) } /* diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index 522185f1..c20898e6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -70,6 +70,9 @@ class MediaPlayerService : Service() { private var notificationBuilder: NotificationCompat.Builder? = null private var rxBusSubscription: CompositeDisposable = CompositeDisposable() + private var currentPlayerState: PlayerState? = null + private var currentTrack: DownloadFile? = null + override fun onBind(intent: Intent): IBinder { return binder } @@ -105,10 +108,6 @@ class MediaPlayerService : Service() { playerStateChangedHandler(it.state, it.track) } - rxBusSubscription += RxBus.currentPlayingObservable.subscribe { - currentPlayingChangedHandler(it.state, it.track) - } - rxBusSubscription += RxBus.mediaSessionTokenObservable.subscribe { mediaSessionToken = it } @@ -349,44 +348,15 @@ class MediaPlayerService : Service() { UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false) } - private fun currentPlayingChangedHandler( - playerState: PlayerState, - currentPlaying: DownloadFile? - ) { - Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song) - Util.broadcastA2dpMetaDataChange( - this@MediaPlayerService, playerPosition, currentPlaying, - downloader.all.size, downloader.currentPlayingIndex + 1 - ) - - // Update widget - val song = currentPlaying?.song - - updateWidget(playerState, song) - - if (currentPlaying != null) { - updateNotification(playerState, currentPlaying) - } else { - stopForeground(true) - isInForeground = false - stopIfIdle() - } - - Timber.d("Processed currently playing track change") - } - private fun playerStateChangedHandler( playerState: PlayerState, currentPlaying: DownloadFile? ) { - val context = this@MediaPlayerService - - if (playerState === PlayerState.PAUSED) { - playbackStateSerializer.serialize( - downloader.playlist, downloader.currentPlayingIndex, playerPosition - ) - } + // AVRCP handles these separately so we must differentiate between the cases + val isStateChanged = playerState != currentPlayerState + val isTrackChanged = currentPlaying != currentTrack + if (!isStateChanged && !isTrackChanged) return val showWhenPaused = playerState !== PlayerState.STOPPED && Settings.isNotificationAlwaysEnabled @@ -394,12 +364,38 @@ class MediaPlayerService : Service() { val show = playerState === PlayerState.STARTED || showWhenPaused val song = currentPlaying?.song - Util.broadcastPlaybackStatusChange(context, playerState) - Util.broadcastA2dpPlayStatusChange( - context, playerState, song, - downloader.playlist.size, - downloader.playlist.indexOf(currentPlaying) + 1, playerPosition - ) + if (isStateChanged) { + when { + playerState === PlayerState.PAUSED -> { + playbackStateSerializer.serialize( + downloader.playlist, downloader.currentPlayingIndex, playerPosition + ) + } + playerState === PlayerState.STARTED -> { + scrobbler.scrobble(currentPlaying, false) + } + playerState === PlayerState.COMPLETED -> { + scrobbler.scrobble(currentPlaying, true) + } + } + + Util.broadcastPlaybackStatusChange(context, playerState) + Util.broadcastA2dpPlayStatusChange( + context, playerState, song, + downloader.playlist.size, + downloader.playlist.indexOf(currentPlaying) + 1, playerPosition + ) + } else { + // State didn't change, only the track + Util.broadcastA2dpMetaDataChange( + this@MediaPlayerService, playerPosition, currentPlaying, + downloader.all.size, downloader.currentPlayingIndex + 1 + ) + } + + if (isTrackChanged) { + Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song) + } // Update widget updateWidget(playerState, song) @@ -415,11 +411,9 @@ class MediaPlayerService : Service() { stopIfIdle() } - if (playerState === PlayerState.STARTED) { - scrobbler.scrobble(currentPlaying, false) - } else if (playerState === PlayerState.COMPLETED) { - scrobbler.scrobble(currentPlaying, true) - } + currentPlayerState = playerState + currentTrack = currentPlaying + Timber.d("Processed player state change") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index fca60614..eeca3ffc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -37,13 +37,6 @@ class RxBus { .replay(1) .autoConnect(0) - val currentPlayingPublisher: PublishSubject = - PublishSubject.create() - val currentPlayingObservable: Observable = - currentPlayingPublisher.observeOn(AndroidSchedulers.mainThread()) - .replay(1) - .autoConnect(0) - val playlistPublisher: PublishSubject> = PublishSubject.create() val playlistObservable: Observable> = From 8830d76497f669f892614274f586b5fbf09699ae Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 9 Nov 2021 18:08:26 +0100 Subject: [PATCH 4/7] Make public playlist immutable (only Downloader can touch it) Remove external usage of playlist_revision --- .../ultrasonic/fragment/DownloadsFragment.kt | 2 +- .../ultrasonic/fragment/PlayerFragment.kt | 21 ++++++---- .../moire/ultrasonic/service/Downloader.kt | 19 +++++---- .../service/MediaPlayerController.kt | 40 ++++++++----------- .../service/MediaPlayerLifecycleSupport.kt | 4 +- .../ultrasonic/service/MediaPlayerService.kt | 28 ++++++------- 6 files changed, 58 insertions(+), 56 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 54d26ef9..b9ca729a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -220,6 +220,6 @@ class DownloadListModel(application: Application) : GenericListModel(application private val downloader by inject() fun getList(): LiveData> { - return downloader.observableList + return downloader.observableDownloads } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 31c61233..3c15c61d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -35,6 +35,7 @@ import android.widget.TextView import android.widget.ViewFlipper import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.navigation.Navigation import com.mobeta.android.dslv.DragSortListView import com.mobeta.android.dslv.DragSortListView.DragSortListener @@ -49,6 +50,7 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import kotlin.math.abs import kotlin.math.max +import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import org.koin.core.component.KoinComponent import org.koin.core.component.get @@ -66,6 +68,7 @@ import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.LocalMediaPlayer import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler @@ -88,8 +91,6 @@ import timber.log.Timber */ @Suppress("LargeClass", "TooManyFunctions", "MagicNumber") class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinComponent { - // Settings - private var currentRevision: Long = 0 private var swipeDistance = 0 private var swipeVelocity = 0 private var jukeboxAvailable = false @@ -419,13 +420,21 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon } } ) - Thread { + + // Observe playlist changes and update the UI + RxBus.playlistObservable.subscribe { + onPlaylistChanged() + } + + // Query the Jukebox state off-thread + viewLifecycleOwner.lifecycleScope.launch { try { jukeboxAvailable = mediaPlayerController.isJukeboxAvailable } catch (all: Exception) { Timber.e(all) } - }.start() + } + view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) } } @@ -797,9 +806,6 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon private fun update(cancel: CancellationToken?) { if (cancel!!.isCancellationRequested) return val mediaPlayerController = mediaPlayerController - if (currentRevision != mediaPlayerController.playListUpdateRevision) { - onPlaylistChanged() - } if (currentPlaying != mediaPlayerController.currentPlaying) { onCurrentChanged() } @@ -914,7 +920,6 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon emptyTextView.isVisible = list.isEmpty() - currentRevision = mediaPlayerController.playListUpdateRevision when (mediaPlayerController.repeatMode) { RepeatMode.OFF -> repeatButton.setImageDrawable( Util.getDrawableFromAttribute( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index a276f1e2..6f9d0184 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -30,14 +30,15 @@ class Downloader( private val externalStorageMonitor: ExternalStorageMonitor, private val localMediaPlayer: LocalMediaPlayer ) : KoinComponent { - val playlist: MutableList = ArrayList() + + private val playlist = mutableListOf() var started: Boolean = false - private val downloadQueue: PriorityQueue = PriorityQueue() - private val activelyDownloading: MutableList = ArrayList() + private val downloadQueue = PriorityQueue() + private val activelyDownloading = mutableListOf() - val observableList: MutableLiveData> = MutableLiveData>() + val observableDownloads = MutableLiveData>() private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() @@ -46,7 +47,7 @@ class Downloader( private var executorService: ScheduledExecutorService? = null private var wifiLock: WifiManager.WifiLock? = null - var playlistUpdateRevision: Long = 0 + private var playlistUpdateRevision: Long = 0 private set(value) { field = value RxBus.playlistPublisher.onNext(playlist) @@ -65,7 +66,7 @@ class Downloader( stop() clearPlaylist() clearBackground() - observableList.value = listOf() + observableDownloads.value = listOf() Timber.i("Downloader destroyed") } @@ -183,7 +184,7 @@ class Downloader( } private fun updateLiveData() { - observableList.postValue(downloads) + observableDownloads.postValue(downloads) } private fun startDownloadOnService(task: DownloadFile) { @@ -268,6 +269,10 @@ class Downloader( return temp.distinct().sorted() } + // Public facing playlist (immutable) + @Synchronized + fun getPlaylist(): List = playlist + @Synchronized fun clearPlaylist() { playlist.clear() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 0e4650c1..05e80330 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -180,7 +180,7 @@ class MediaPlayerController( downloader.addToPlaylist(filteredSongs, save, autoPlay, playNext, newPlaylist) jukeboxMediaPlayer.updatePlaylist() if (shuffle) shuffle() - val isLastTrack = (downloader.playlist.size - 1 == downloader.currentPlayingIndex) + val isLastTrack = (downloader.getPlaylist().size - 1 == downloader.currentPlayingIndex) if (!playNext && !autoPlay && isLastTrack) { val mediaPlayerService = runningInstance @@ -190,15 +190,15 @@ class MediaPlayerController( if (autoPlay) { play(0) } else { - if (localMediaPlayer.currentPlaying == null && downloader.playlist.size > 0) { - localMediaPlayer.currentPlaying = downloader.playlist[0] - downloader.playlist[0].setPlaying(true) + if (localMediaPlayer.currentPlaying == null && downloader.getPlaylist().isNotEmpty()) { + localMediaPlayer.currentPlaying = downloader.getPlaylist()[0] + downloader.getPlaylist()[0].setPlaying(true) } downloader.checkDownloads() } playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) @@ -210,7 +210,7 @@ class MediaPlayerController( val filteredSongs = songs.filterNotNull() downloader.downloadBackground(filteredSongs, save) playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) @@ -241,7 +241,7 @@ class MediaPlayerController( fun shuffle() { downloader.shuffle() playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) @@ -270,7 +270,7 @@ class MediaPlayerController( downloader.clearPlaylist() if (serialize) { playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) } @@ -281,16 +281,11 @@ class MediaPlayerController( @Synchronized fun clearIncomplete() { reset() - val iterator = downloader.playlist.iterator() - while (iterator.hasNext()) { - val downloadFile = iterator.next() - if (!downloadFile.isCompleteFileAvailable) { - iterator.remove() - } - } + + downloader.clearIncomplete() playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) @@ -307,7 +302,7 @@ class MediaPlayerController( downloader.removeFromPlaylist(downloadFile) playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) @@ -359,12 +354,12 @@ class MediaPlayerController( when (repeatMode) { RepeatMode.SINGLE, RepeatMode.OFF -> { // Play next if exists - if (index + 1 >= 0 && index + 1 < downloader.playlist.size) { + if (index + 1 >= 0 && index + 1 < downloader.getPlaylist().size) { play(index + 1) } } RepeatMode.ALL -> { - play((index + 1) % downloader.playlist.size) + play((index + 1) % downloader.getPlaylist().size) } else -> { } @@ -492,16 +487,13 @@ class MediaPlayerController( } val playlistSize: Int - get() = downloader.playlist.size + get() = downloader.getPlaylist().size val currentPlayingNumberOnPlaylist: Int get() = downloader.currentPlayingIndex val playList: List - get() = downloader.playlist - - val playListUpdateRevision: Long - get() = downloader.playlistUpdateRevision + get() = downloader.getPlaylist() val playListDuration: Long get() = downloader.downloadListDuration diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index f48ec305..d52927a4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -70,7 +70,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { // Work-around: Serialize again, as the restore() method creates a // serialization without current playing info. playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, mediaPlayerController.playerPosition ) @@ -87,7 +87,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { if (!created) return playbackStateSerializer.serializeNow( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, mediaPlayerController.playerPosition ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index c20898e6..e503b36f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -87,7 +87,7 @@ class MediaPlayerService : Service() { localMediaPlayer.onPrepared = { playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) @@ -198,7 +198,7 @@ class MediaPlayerService : Service() { @Synchronized fun setCurrentPlaying(currentPlayingIndex: Int) { try { - localMediaPlayer.setCurrentPlaying(downloader.playlist[currentPlayingIndex]) + localMediaPlayer.setCurrentPlaying(downloader.getPlaylist()[currentPlayingIndex]) } catch (ignored: IndexOutOfBoundsException) { } } @@ -215,7 +215,7 @@ class MediaPlayerService : Service() { if (index != -1) { when (Settings.repeatMode) { RepeatMode.OFF -> index += 1 - RepeatMode.ALL -> index = (index + 1) % downloader.playlist.size + RepeatMode.ALL -> index = (index + 1) % downloader.getPlaylist().size RepeatMode.SINGLE -> { } else -> { @@ -224,8 +224,8 @@ class MediaPlayerService : Service() { } localMediaPlayer.clearNextPlaying(false) - if (index < downloader.playlist.size && index != -1) { - localMediaPlayer.setNextPlaying(downloader.playlist[index]) + if (index < downloader.getPlaylist().size && index != -1) { + localMediaPlayer.setNextPlaying(downloader.getPlaylist()[index]) } else { localMediaPlayer.clearNextPlaying(true) } @@ -278,7 +278,7 @@ class MediaPlayerService : Service() { @Synchronized fun play(index: Int, start: Boolean) { Timber.v("play requested for %d", index) - if (index < 0 || index >= downloader.playlist.size) { + if (index < 0 || index >= downloader.getPlaylist().size) { resetPlayback() } else { setCurrentPlaying(index) @@ -286,7 +286,7 @@ class MediaPlayerService : Service() { if (jukeboxMediaPlayer.isEnabled) { jukeboxMediaPlayer.skip(index, 0) } else { - localMediaPlayer.play(downloader.playlist[index]) + localMediaPlayer.play(downloader.getPlaylist()[index]) } } downloader.checkDownloads() @@ -299,7 +299,7 @@ class MediaPlayerService : Service() { localMediaPlayer.reset() localMediaPlayer.setCurrentPlaying(null) playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) } @@ -368,7 +368,7 @@ class MediaPlayerService : Service() { when { playerState === PlayerState.PAUSED -> { playbackStateSerializer.serialize( - downloader.playlist, downloader.currentPlayingIndex, playerPosition + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) } playerState === PlayerState.STARTED -> { @@ -382,8 +382,8 @@ class MediaPlayerService : Service() { Util.broadcastPlaybackStatusChange(context, playerState) Util.broadcastA2dpPlayStatusChange( context, playerState, song, - downloader.playlist.size, - downloader.playlist.indexOf(currentPlaying) + 1, playerPosition + downloader.getPlaylist().size, + downloader.getPlaylist().indexOf(currentPlaying) + 1, playerPosition ) } else { // State didn't change, only the track @@ -434,7 +434,7 @@ class MediaPlayerService : Service() { if (index != -1) { when (Settings.repeatMode) { RepeatMode.OFF -> { - if (index + 1 < 0 || index + 1 >= downloader.playlist.size) { + if (index + 1 < 0 || index + 1 >= downloader.getPlaylist().size) { if (Settings.shouldClearPlaylist) { clear(true) jukeboxMediaPlayer.updatePlaylist() @@ -445,7 +445,7 @@ class MediaPlayerService : Service() { } } RepeatMode.ALL -> { - play((index + 1) % downloader.playlist.size) + play((index + 1) % downloader.getPlaylist().size) } RepeatMode.SINGLE -> play(index) else -> { @@ -464,7 +464,7 @@ class MediaPlayerService : Service() { setNextPlaying() if (serialize) { playbackStateSerializer.serialize( - downloader.playlist, + downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition ) } From bb36116d70c07e0fc40db3a20d90abb17ee30729 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 9 Nov 2021 22:20:41 +0100 Subject: [PATCH 5/7] Add .ifNotNull extension function as a more readable alternative to ?.let --- .../ultrasonic/domain/APIPlaylistConverter.kt | 3 ++- .../ultrasonic/domain/APIShareConverter.kt | 7 ++++--- .../fragment/TrackCollectionFragment.kt | 2 +- .../service/CommunicationErrorHandler.kt | 4 +++- .../service/MediaPlayerLifecycleSupport.kt | 3 ++- .../moire/ultrasonic/subsonic/ShareHandler.kt | 3 ++- .../ultrasonic/util/MediaSessionHandler.kt | 19 +++++++------------ .../kotlin/org/moire/ultrasonic/util/Util.kt | 9 ++++++++- 8 files changed, 29 insertions(+), 21 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverter.kt index 7699acf3..57f7e29d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverter.kt @@ -6,6 +6,7 @@ package org.moire.ultrasonic.domain import java.text.SimpleDateFormat import kotlin.LazyThreadSafetyMode.NONE import org.moire.ultrasonic.api.subsonic.models.Playlist as APIPlaylist +import org.moire.ultrasonic.util.Util.ifNotNull internal val playlistDateFormat by lazy(NONE) { SimpleDateFormat.getInstance() } @@ -17,7 +18,7 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory( fun APIPlaylist.toDomainEntity(): Playlist = Playlist( this.id, this.name, this.owner, this.comment, this.songCount.toString(), - this.created?.let { playlistDateFormat.format(it.time) } ?: "", + this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "", public ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIShareConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIShareConverter.kt index 36486783..408f42f7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIShareConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIShareConverter.kt @@ -5,6 +5,7 @@ package org.moire.ultrasonic.domain import java.text.SimpleDateFormat import kotlin.LazyThreadSafetyMode.NONE import org.moire.ultrasonic.api.subsonic.models.Share as APIShare +import org.moire.ultrasonic.util.Util.ifNotNull internal val shareTimeFormat by lazy(NONE) { SimpleDateFormat.getInstance() } @@ -13,11 +14,11 @@ fun List.toDomainEntitiesList(): List = this.map { } fun APIShare.toDomainEntity(): Share = Share( - created = this@toDomainEntity.created?.let { shareTimeFormat.format(it.time) }, + created = this@toDomainEntity.created.ifNotNull { shareTimeFormat.format(it.time) }, description = this@toDomainEntity.description, - expires = this@toDomainEntity.expires?.let { shareTimeFormat.format(it.time) }, + expires = this@toDomainEntity.expires.ifNotNull { shareTimeFormat.format(it.time) }, id = this@toDomainEntity.id, - lastVisited = this@toDomainEntity.lastVisited?.let { shareTimeFormat.format(it.time) }, + lastVisited = this@toDomainEntity.lastVisited.ifNotNull { shareTimeFormat.format(it.time) }, url = this@toDomainEntity.url, username = this@toDomainEntity.username, visitCount = this@toDomainEntity.visitCount.toLong(), diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index c6681559..9d785a30 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -211,7 +211,7 @@ class TrackCollectionFragment : Fragment() { val handler = CoroutineExceptionHandler { _, exception -> Handler(Looper.getMainLooper()).post { - context?.let { CommunicationErrorHandler.handleError(exception, it) } + CommunicationErrorHandler.handleError(exception, context) } refreshAlbumListView!!.isRefreshing = false } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt index 1b876fb5..bfda4aab 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt @@ -39,9 +39,11 @@ import timber.log.Timber */ class CommunicationErrorHandler { companion object { - fun handleError(error: Throwable?, context: Context) { + fun handleError(error: Throwable?, context: Context?) { Timber.w(error) + if (context == null) return + AlertDialog.Builder(context) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.error_label) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index d52927a4..379b6d60 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -22,6 +22,7 @@ import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.util.Util.ifNotNull import timber.log.Timber /** @@ -113,7 +114,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { if (intentAction == Constants.CMD_PROCESS_KEYCODE) { if (intent.extras != null) { val event = intent.extras!![Intent.EXTRA_KEY_EVENT] as KeyEvent? - event?.let { handleKeyEvent(it) } + event.ifNotNull { handleKeyEvent(it) } } } else { handleUltrasonicIntent(intentAction) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt index 883aeeac..13f9922f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt @@ -26,6 +26,7 @@ import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.ShareDetails import org.moire.ultrasonic.util.TimeSpan import org.moire.ultrasonic.util.TimeSpanPicker +import org.moire.ultrasonic.util.Util.ifNotNull /** * This class handles sharing items in the media library @@ -79,7 +80,7 @@ class ShareHandler(val context: Context) { if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null if (shareDetails.Entries.isEmpty()) { - fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)?.let { + fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID).ifNotNull { ids.add(it) } } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt index 4768f6eb..f7bdf220 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt @@ -28,6 +28,7 @@ import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign +import org.moire.ultrasonic.util.Util.ifNotNull import timber.log.Timber private const val INTENT_CODE_MEDIA_BUTTON = 161 @@ -150,7 +151,7 @@ class MediaSessionHandler : KoinComponent { // 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? - event?.let { RxBus.mediaButtonEventPublisher.onNext(it) } + event.ifNotNull { RxBus.mediaButtonEventPublisher.onNext(it) } return true } @@ -257,13 +258,10 @@ class MediaSessionHandler : KoinComponent { val index = cachedPlaylist?.indexOf(currentPlaying) cachedPlayingIndex = if (index == null || index < 0) null else index.toLong() - cachedPlaylist?.let { setMediaSessionQueue(it) } + cachedPlaylist.ifNotNull { setMediaSessionQueue(it) } - if ( - cachedPlayingIndex != null && cachedPlaylist != null && - !Settings.shouldDisableNowPlayingListSending - ) - cachedPlayingIndex?.let { playbackStateBuilder.setActiveQueueItemId(it) } + if (cachedPlaylist != null && !Settings.shouldDisableNowPlayingListSending) + cachedPlayingIndex.ifNotNull { playbackStateBuilder.setActiveQueueItemId(it) } // Save the playback state mediaSession?.setPlaybackState(playbackStateBuilder.build()) @@ -296,11 +294,8 @@ class MediaSessionHandler : KoinComponent { playbackStateBuilder.setState(playbackState!!, cachedPosition, 1.0f) playbackStateBuilder.setActions(playbackActions!!) - if ( - cachedPlayingIndex != null && cachedPlaylist != null && - !Settings.shouldDisableNowPlayingListSending - ) - playbackStateBuilder.setActiveQueueItemId(cachedPlayingIndex!!) + if (cachedPlaylist != null && !Settings.shouldDisableNowPlayingListSending) + cachedPlayingIndex.ifNotNull { playbackStateBuilder.setActiveQueueItemId(it) } mediaSession?.setPlaybackState(playbackStateBuilder.build()) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 26ae9369..1d4b4b7f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -538,7 +538,6 @@ object Util { } /** - * * Broadcasts the given song info as the new song being played. */ fun broadcastNewTrackInfo(context: Context, song: MusicDirectory.Entry?) { @@ -957,6 +956,14 @@ object Util { return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } + /** + * Executes the given block if this is not null. + * @return: the return of the block, or null if this is null + */ + fun T?.ifNotNull(block: (T) -> R): R? { + return this?.let(block) + } + data class NetworkInfo( var connected: Boolean = false, var unmetered: Boolean = false From e019ec788d874f80145bce06b19819488737ba44 Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 15 Nov 2021 18:20:26 +0100 Subject: [PATCH 6/7] Fixed subscription --- .../kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 3c15c61d..14cbbdf5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -39,6 +39,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.Navigation import com.mobeta.android.dslv.DragSortListView import com.mobeta.android.dslv.DragSortListView.DragSortListener +import io.reactivex.rxjava3.disposables.Disposable import java.text.DateFormat import java.text.SimpleDateFormat import java.util.ArrayList @@ -112,6 +113,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon private var currentPlaying: DownloadFile? = null private var currentSong: MusicDirectory.Entry? = null private var onProgressChangedTask: SilentBackgroundTask? = null + private var rxBusSubscription: Disposable? = null // Views and UI Elements private lateinit var visualizerViewLayout: LinearLayout @@ -422,7 +424,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon ) // Observe playlist changes and update the UI - RxBus.playlistObservable.subscribe { + rxBusSubscription = RxBus.playlistObservable.subscribe { onPlaylistChanged() } @@ -488,6 +490,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon } override fun onDestroyView() { + rxBusSubscription?.dispose() cancellationToken.cancel() super.onDestroyView() } From 00781ba7de1651aae5dae411dfbc8c8eb3ebd39f Mon Sep 17 00:00:00 2001 From: tzugen <67737443+tzugen@users.noreply.github.com> Date: Tue, 16 Nov 2021 12:33:50 +0100 Subject: [PATCH 7/7] Add a comment --- .../src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 6f9d0184..a1aa4bec 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -38,6 +38,8 @@ class Downloader( private val downloadQueue = PriorityQueue() private val activelyDownloading = mutableListOf() + // TODO: The playlist is now published with RX, while the observableDownloads is using LiveData. + // Use the same for both val observableDownloads = MutableLiveData>() private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()