From ffb2d5988669ff015b5170c4afa8e3f8b83e45f2 Mon Sep 17 00:00:00 2001 From: Nite Date: Tue, 2 Nov 2021 17:45:01 +0100 Subject: [PATCH] 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() -}