Updated Events to ReactiveX

Minor fixes
This commit is contained in:
Nite 2021-11-02 17:45:01 +01:00
parent fec2d78d30
commit ffb2d59886
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
17 changed files with 530 additions and 621 deletions

View File

@ -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<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<NowPlayingEventDistributor> 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;
}
}

View File

@ -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()
}

View File

@ -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() }
}

View File

@ -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
}
}

View File

@ -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<MediaSessionEventDistributor>()
private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
private val mediaSessionHandler by inject<MediaSessionHandler>()
private val mediaPlayerController by inject<MediaPlayerController>()
@ -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()

View File

@ -31,6 +31,7 @@ class Downloader(
private val localMediaPlayer: LocalMediaPlayer
) : KoinComponent {
val playlist: MutableList<DownloadFile> = ArrayList()
var started: Boolean = false
private val downloadQueue: PriorityQueue<DownloadFile> = PriorityQueue<DownloadFile>()
@ -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<MusicDirectory.Entry>, 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

View File

@ -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<AudioFocusHandler>()
private val context by inject<Context>()
private val mediaSessionHandler by inject<MediaSessionHandler>()
@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) {

View File

@ -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()
}

View File

@ -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<PlaybackStateSerializer>()
private val mediaPlayerController by inject<MediaPlayerController>()
private val downloader by inject<Downloader>()
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
private var created = false
private var headsetEventReceiver: BroadcastReceiver? = null

View File

@ -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<ShufflePlayBuffer>()
private val downloader by inject<Downloader>()
private val localMediaPlayer by inject<LocalMediaPlayer>()
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
private val mediaSessionHandler by inject<MediaSessionHandler>()
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) {

View File

@ -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<Context>()
private val mediaSessionHandler by inject<MediaSessionHandler>()
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)
}
}

View File

@ -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<MediaSessionCompat.Token> =
PublishSubject.create()
val mediaSessionTokenObservable: Observable<MediaSessionCompat.Token> =
mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect()
.doOnEach { Timber.d("RxBus mediaSessionTokenPublisher onEach $it")}
class RxBus {
companion object {
var mediaSessionTokenPublisher: PublishSubject<MediaSessionCompat.Token> =
PublishSubject.create()
val mediaSessionTokenObservable: Observable<MediaSessionCompat.Token> =
mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect(0)
val mediaButtonEventPublisher: PublishSubject<KeyEvent> =
PublishSubject.create()
val mediaButtonEventObservable: Observable<KeyEvent> =
mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread())
.doOnEach { Timber.d("RxBus mediaButtonEventPublisher onEach $it")}
val mediaButtonEventPublisher: PublishSubject<KeyEvent> =
PublishSubject.create()
val mediaButtonEventObservable: Observable<KeyEvent> =
mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread())
val themeChangedEventPublisher: PublishSubject<Unit> =
PublishSubject.create()
val themeChangedEventObservable: Observable<Unit> =
themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
.doOnEach { Timber.d("RxBus themeChangedEventPublisher onEach $it")}
val themeChangedEventPublisher: PublishSubject<Unit> =
PublishSubject.create()
val themeChangedEventObservable: Observable<Unit> =
themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
val dismissNowPlayingCommandPublisher: PublishSubject<Unit> =
PublishSubject.create()
val dismissNowPlayingCommandObservable: Observable<Unit> =
dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread())
.doOnEach { Timber.d("RxBus dismissNowPlayingCommandPublisher onEach $it")}
val playerStatePublisher: PublishSubject<StateWithTrack> =
PublishSubject.create()
val playerStateObservable: Observable<StateWithTrack> =
playerStatePublisher.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect(0)
fun releaseMediaSessionToken() { mediaSessionTokenPublisher = PublishSubject.create() }
val currentPlayingPublisher: PublishSubject<StateWithTrack> =
PublishSubject.create()
val currentPlayingObservable: Observable<StateWithTrack> =
currentPlayingPublisher.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect(0)
val playlistPublisher: PublishSubject<List<DownloadFile>> =
PublishSubject.create()
val playlistObservable: Observable<List<DownloadFile>> =
playlistPublisher.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect(0)
val playbackPositionPublisher: PublishSubject<Int> =
PublishSubject.create()
val playbackPositionObservable: Observable<Int> =
playbackPositionPublisher.observeOn(AndroidSchedulers.mainThread())
.throttleFirst(1, TimeUnit.SECONDS)
.replay(1)
.autoConnect(0)
// Commands
val dismissNowPlayingCommandPublisher: PublishSubject<Unit> =
PublishSubject.create()
val dismissNowPlayingCommandObservable: Observable<Unit> =
dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread())
val playFromMediaIdCommandPublisher: PublishSubject<Pair<String?, Bundle?>> =
PublishSubject.create()
val playFromMediaIdCommandObservable: Observable<Pair<String?, Bundle?>> =
playFromMediaIdCommandPublisher.observeOn(AndroidSchedulers.mainThread())
val playFromSearchCommandPublisher: PublishSubject<Pair<String?, Bundle?>> =
PublishSubject.create()
val playFromSearchCommandObservable: Observable<Pair<String?, Bundle?>> =
playFromSearchCommandPublisher.observeOn(AndroidSchedulers.mainThread())
val skipToQueueItemCommandPublisher: PublishSubject<Long> =
PublishSubject.create()
val skipToQueueItemCommandObservable: Observable<Long> =
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)
}

View File

@ -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<MediaSessionEventListener> =
listOf<MediaSessionEventListener>().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) }
}
}

View File

@ -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?) {}
}

View File

@ -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<MediaSessionEventDistributor>()
private val applicationContext by inject<Context>()
private var referenceCount: Int = 0
private var cachedPlaylist: List<MediaSessionCompat.QueueItem>? = null
private var playbackPositionDelayCount: Int = 0
private var cachedPlaylist: List<DownloadFile>? = 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<MusicDirectory.Entry>) {
// 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<DownloadFile>) {
cachedPlaylist = playlist
setMediaSessionQueue(playlist)
}
private fun setMediaSessionQueue(queue: List<MediaSessionCompat.QueueItem>?) {
private fun setMediaSessionQueue(playlist: List<DownloadFile>) {
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 (

View File

@ -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<NowPlayingEventListener> =
listOf<NowPlayingEventListener>().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() }
}
}

View File

@ -1,9 +0,0 @@
package org.moire.ultrasonic.util
/**
* Callback interface for Now Playing event subscribers
*/
interface NowPlayingEventListener {
fun onHideNowPlaying()
fun onShowNowPlaying()
}