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.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.NowPlayingEventListener
import org.moire.ultrasonic.util.PermissionUtil import org.moire.ultrasonic.util.PermissionUtil
import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
@ -74,14 +72,13 @@ class NavigationActivity : AppCompatActivity() {
private var headerBackgroundImage: ImageView? = null private var headerBackgroundImage: ImageView? = null
private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var nowPlayingEventListener: NowPlayingEventListener
private var themeChangedEventSubscription: Disposable? = null private var themeChangedEventSubscription: Disposable? = null
private var playerStateSubscription: Disposable? = null
private val serverSettingsModel: ServerSettingsModel by viewModel() private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
private val mediaPlayerController: MediaPlayerController by inject() private val mediaPlayerController: MediaPlayerController by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject() private val imageLoaderProvider: ImageLoaderProvider by inject()
private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject()
private val permissionUtil: PermissionUtil by inject() private val permissionUtil: PermissionUtil by inject()
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
private val serverRepository: ServerSettingDao by inject() private val serverRepository: ServerSettingDao by inject()
@ -173,23 +170,17 @@ class NavigationActivity : AppCompatActivity() {
hideNowPlaying() hideNowPlaying()
} }
nowPlayingEventListener = object : NowPlayingEventListener { playerStateSubscription = RxBus.playerStateObservable.subscribe {
if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED)
override fun onHideNowPlaying() {
hideNowPlaying()
}
override fun onShowNowPlaying() {
showNowPlaying() showNowPlaying()
} else
hideNowPlaying()
} }
themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe { themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe {
recreate() recreate()
} }
nowPlayingEventDistributor.subscribe(nowPlayingEventListener)
serverRepository.liveServerCount().observe( serverRepository.liveServerCount().observe(
this, this,
{ count -> { count ->
@ -236,8 +227,8 @@ class NavigationActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
themeChangedEventSubscription?.dispose() themeChangedEventSubscription?.dispose()
playerStateSubscription?.dispose()
imageLoaderProvider.clearImageLoader() imageLoaderProvider.clearImageLoader()
permissionUtil.onForegroundApplicationStopped() permissionUtil.onForegroundApplicationStopped()
} }

View File

@ -4,9 +4,7 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.MediaSessionEventDistributor
import org.moire.ultrasonic.util.MediaSessionHandler import org.moire.ultrasonic.util.MediaSessionHandler
import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.PermissionUtil import org.moire.ultrasonic.util.PermissionUtil
/** /**
@ -16,7 +14,5 @@ val applicationModule = module {
single { ActiveServerProvider(get()) } single { ActiveServerProvider(get()) }
single { ImageLoaderProvider(androidContext()) } single { ImageLoaderProvider(androidContext()) }
single { PermissionUtil(androidContext()) } single { PermissionUtil(androidContext()) }
single { NowPlayingEventDistributor() }
single { MediaSessionEventDistributor() }
single { MediaSessionHandler() } 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.os.Handler
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.session.MediaSessionCompat
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job 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.MusicDirectory
import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult 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.MediaSessionHandler
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -74,8 +71,6 @@ private const val SEARCH_LIMIT = 10
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
class AutoMediaBrowserService : MediaBrowserServiceCompat() { class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private lateinit var mediaSessionEventListener: MediaSessionEventListener
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>() private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
private val mediaSessionHandler by inject<MediaSessionHandler>() private val mediaSessionHandler by inject<MediaSessionHandler>()
private val mediaPlayerController by inject<MediaPlayerController>() private val mediaPlayerController by inject<MediaPlayerController>()
@ -94,19 +89,44 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private val useId3Tags get() = Settings.shouldUseId3Tags private val useId3Tags get() = Settings.shouldUseId3Tags
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
private var mediaSessionTokenSubscription: Disposable? = null private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
@Suppress("MagicNumber") @Suppress("MagicNumber")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
mediaSessionTokenSubscription = RxBus.mediaSessionTokenObservable.subscribe { rxBusSubscription += RxBus.mediaSessionTokenObservable.subscribe {
if (sessionToken == null) sessionToken = it if (sessionToken == null) sessionToken = it
} }
mediaSessionEventListener = object : MediaSessionEventListener { rxBusSubscription += RxBus.playFromMediaIdCommandObservable.subscribe {
playFromMediaId(it.first)
}
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) { rxBusSubscription += RxBus.playFromSearchCommandObservable.subscribe {
playFromSearchCommand(it.first)
}
mediaSessionHandler.initialize()
val handler = Handler()
handler.postDelayed(
{
// Ultrasonic may be started from Android Auto. This boots up the necessary components.
Timber.d(
"AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService..."
)
lifecycleSupport.onCreate()
MediaPlayerService.getInstance()
},
100
)
Timber.i("AutoMediaBrowserService onCreate finished")
}
@Suppress("MagicNumber", "ComplexMethod")
private fun playFromMediaId(mediaId: String?) {
Timber.d( Timber.d(
"AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s", "AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s",
mediaId mediaId
@ -139,7 +159,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
} }
} }
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) { private fun playFromSearchCommand(query: String?) {
Timber.d("AutoMediaBrowserService onPlayFromSearchRequested query: %s", query) Timber.d("AutoMediaBrowserService onPlayFromSearchRequested query: %s", query)
if (query.isNullOrBlank()) playRandomSongs() if (query.isNullOrBlank()) playRandomSongs()
@ -161,31 +181,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
} }
} }
} }
}
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
mediaSessionHandler.initialize()
val handler = Handler()
handler.postDelayed(
{
// Ultrasonic may be started from Android Auto. This boots up the necessary components.
Timber.d(
"AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService..."
)
lifecycleSupport.onCreate()
MediaPlayerService.getInstance()
},
100
)
Timber.i("AutoMediaBrowserService onCreate finished")
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
mediaSessionTokenSubscription?.dispose() rxBusSubscription.dispose()
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
mediaSessionHandler.release() mediaSessionHandler.release()
serviceJob.cancel() serviceJob.cancel()

View File

@ -31,6 +31,7 @@ class Downloader(
private val localMediaPlayer: LocalMediaPlayer private val localMediaPlayer: LocalMediaPlayer
) : KoinComponent { ) : KoinComponent {
val playlist: MutableList<DownloadFile> = ArrayList() val playlist: MutableList<DownloadFile> = ArrayList()
var started: Boolean = false var started: Boolean = false
private val downloadQueue: PriorityQueue<DownloadFile> = PriorityQueue<DownloadFile>() private val downloadQueue: PriorityQueue<DownloadFile> = PriorityQueue<DownloadFile>()
@ -46,7 +47,10 @@ class Downloader(
private var wifiLock: WifiManager.WifiLock? = null private var wifiLock: WifiManager.WifiLock? = null
var playlistUpdateRevision: Long = 0 var playlistUpdateRevision: Long = 0
private set private set(value) {
field = value
RxBus.playlistPublisher.onNext(playlist)
}
val downloadChecker = Runnable { val downloadChecker = Runnable {
try { try {
@ -349,6 +353,20 @@ class Downloader(
checkDownloads() 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 @Synchronized
fun downloadBackground(songs: List<MusicDirectory.Entry>, save: Boolean) { fun downloadBackground(songs: List<MusicDirectory.Entry>, save: Boolean) {
@ -429,18 +447,21 @@ class Downloader(
playlistUpdateRevision++ playlistUpdateRevision++
} }
} }
if (revisionBefore != playlistUpdateRevision) { if (revisionBefore != playlistUpdateRevision) {
jukeboxMediaPlayer.updatePlaylist() jukeboxMediaPlayer.updatePlaylist()
} }
if (wasEmpty && playlist.isNotEmpty()) { if (wasEmpty && playlist.isNotEmpty()) {
if (jukeboxMediaPlayer.isEnabled) { if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.skip(0, 0) jukeboxMediaPlayer.skip(0, 0)
localMediaPlayer.setPlayerState(PlayerState.STARTED) localMediaPlayer.setPlayerState(PlayerState.STARTED, playlist[0])
} else { } else {
localMediaPlayer.play(playlist[0]) localMediaPlayer.play(playlist[0])
} }
} }
} }
companion object { companion object {
const val PARALLEL_DOWNLOADS = 3 const val PARALLEL_DOWNLOADS = 3
const val CHECK_INTERVAL = 5L 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.domain.PlayerState
import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.CancellableTask
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.MediaSessionHandler
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.StreamProxy import org.moire.ultrasonic.util.StreamProxy
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -46,17 +45,10 @@ class LocalMediaPlayer : KoinComponent {
private val audioFocusHandler by inject<AudioFocusHandler>() private val audioFocusHandler by inject<AudioFocusHandler>()
private val context by inject<Context>() private val context by inject<Context>()
private val mediaSessionHandler by inject<MediaSessionHandler>()
@JvmField
var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null
@JvmField @JvmField
var onSongCompleted: ((DownloadFile?) -> Unit?)? = null var onSongCompleted: ((DownloadFile?) -> Unit?)? = null
@JvmField
var onPlayerStateChanged: ((PlayerState, DownloadFile?) -> Unit?)? = null
@JvmField @JvmField
var onPrepared: (() -> Any?)? = null var onPrepared: (() -> Any?)? = null
@ -64,6 +56,7 @@ class LocalMediaPlayer : KoinComponent {
var onNextSongRequested: Runnable? = null var onNextSongRequested: Runnable? = null
@JvmField @JvmField
@Volatile
var playerState = PlayerState.IDLE var playerState = PlayerState.IDLE
@JvmField @JvmField
@ -132,7 +125,6 @@ class LocalMediaPlayer : KoinComponent {
// Calling reset() will result in changing this player's state. If we allow // Calling reset() will result in changing this player's state. If we allow
// the onPlayerStateChanged callback, then the state change will cause this // the onPlayerStateChanged callback, then the state change will cause this
// to resurrect the media session which has just been destroyed. // to resurrect the media session which has just been destroyed.
onPlayerStateChanged = null
reset() reset()
try { try {
val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
@ -164,21 +156,17 @@ class LocalMediaPlayer : KoinComponent {
} }
@Synchronized @Synchronized
fun setPlayerState(playerState: PlayerState) { fun setPlayerState(playerState: PlayerState, track: DownloadFile?) {
Timber.i("%s -> %s (%s)", this.playerState.name, playerState.name, currentPlaying) Timber.i("%s -> %s (%s)", this.playerState.name, playerState.name, track)
synchronized(playerState) {
this.playerState = playerState this.playerState = playerState
}
if (playerState === PlayerState.STARTED) { if (playerState === PlayerState.STARTED) {
audioFocusHandler.requestAudioFocus() audioFocusHandler.requestAudioFocus()
} }
if (onPlayerStateChanged != null) { RxBus.playerStatePublisher.onNext(RxBus.StateWithTrack(playerState, track))
val mainHandler = Handler(context.mainLooper)
val myRunnable = Runnable {
onPlayerStateChanged?.invoke(playerState, currentPlaying)
}
mainHandler.post(myRunnable)
}
if (playerState === PlayerState.STARTED && positionCache == null) { if (playerState === PlayerState.STARTED && positionCache == null) {
positionCache = PositionCache() positionCache = PositionCache()
val thread = Thread(positionCache) val thread = Thread(positionCache)
@ -194,14 +182,10 @@ class LocalMediaPlayer : KoinComponent {
*/ */
@Synchronized @Synchronized
fun setCurrentPlaying(currentPlaying: DownloadFile?) { 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 this.currentPlaying = currentPlaying
RxBus.currentPlayingPublisher.onNext(RxBus.StateWithTrack(playerState, currentPlaying))
if (onCurrentPlayingChanged != null) {
val mainHandler = Handler(context.mainLooper)
val myRunnable = Runnable { onCurrentPlayingChanged!!(currentPlaying) }
mainHandler.post(myRunnable)
}
} }
/* /*
@ -262,7 +246,7 @@ class LocalMediaPlayer : KoinComponent {
mediaPlayer = nextMediaPlayer!! mediaPlayer = nextMediaPlayer!!
setCurrentPlaying(nextPlaying) setCurrentPlaying(nextPlaying)
setPlayerState(PlayerState.STARTED) setPlayerState(PlayerState.STARTED, currentPlaying)
attachHandlersToPlayer(mediaPlayer, nextPlaying!!, false) attachHandlersToPlayer(mediaPlayer, nextPlaying!!, false)
@ -343,7 +327,7 @@ class LocalMediaPlayer : KoinComponent {
@Synchronized @Synchronized
private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) { private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) {
if (playerState !== PlayerState.PREPARED) { if (playerState !== PlayerState.PREPARED && !fileToPlay.isWorkDone) {
reset() reset()
bufferTask = BufferTask(fileToPlay, position, autoStart) bufferTask = BufferTask(fileToPlay, position, autoStart)
bufferTask!!.start() bufferTask!!.start()
@ -354,6 +338,7 @@ class LocalMediaPlayer : KoinComponent {
@Synchronized @Synchronized
private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) { 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. // In many cases we will be resetting the mediaPlayer a second time here.
// figure out if we can remove this call... // figure out if we can remove this call...
@ -368,7 +353,6 @@ class LocalMediaPlayer : KoinComponent {
downloadFile.updateModificationDate() downloadFile.updateModificationDate()
mediaPlayer.setOnCompletionListener(null) mediaPlayer.setOnCompletionListener(null)
setPlayerState(PlayerState.IDLE)
setAudioAttributes(mediaPlayer) setAudioAttributes(mediaPlayer)
var dataSource = file.path var dataSource = file.path
@ -394,7 +378,7 @@ class LocalMediaPlayer : KoinComponent {
Timber.i("Preparing media player") Timber.i("Preparing media player")
mediaPlayer.setDataSource(dataSource) mediaPlayer.setDataSource(dataSource)
setPlayerState(PlayerState.PREPARING) setPlayerState(PlayerState.PREPARING, downloadFile)
mediaPlayer.setOnBufferingUpdateListener { mp, percent -> mediaPlayer.setOnBufferingUpdateListener { mp, percent ->
val song = downloadFile.song val song = downloadFile.song
@ -412,7 +396,7 @@ class LocalMediaPlayer : KoinComponent {
mediaPlayer.setOnPreparedListener { mediaPlayer.setOnPreparedListener {
Timber.i("Media player prepared") 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 // Populate seek bar secondary progress if we have a complete file for consistency
if (downloadFile.isWorkDone) { if (downloadFile.isWorkDone) {
@ -427,9 +411,9 @@ class LocalMediaPlayer : KoinComponent {
cachedPosition = position cachedPosition = position
if (start) { if (start) {
mediaPlayer.start() mediaPlayer.start()
setPlayerState(PlayerState.STARTED) setPlayerState(PlayerState.STARTED, downloadFile)
} else { } else {
setPlayerState(PlayerState.PAUSED) setPlayerState(PlayerState.PAUSED, downloadFile)
} }
} }
@ -437,6 +421,7 @@ class LocalMediaPlayer : KoinComponent {
onPrepared onPrepared
} }
} }
attachHandlersToPlayer(mediaPlayer, downloadFile, partial) attachHandlersToPlayer(mediaPlayer, downloadFile, partial)
mediaPlayer.prepareAsync() mediaPlayer.prepareAsync()
} catch (x: Exception) { } catch (x: Exception) {
@ -527,7 +512,7 @@ class LocalMediaPlayer : KoinComponent {
Timber.i("Ending position %d of %d", pos, duration) Timber.i("Ending position %d of %d", pos, duration)
if (!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000) { if (!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000) {
setPlayerState(PlayerState.COMPLETED) setPlayerState(PlayerState.COMPLETED, downloadFile)
if (Settings.gaplessPlayback && if (Settings.gaplessPlayback &&
nextPlaying != null && nextPlaying != null &&
nextPlayerState === PlayerState.PREPARED nextPlayerState === PlayerState.PREPARED
@ -574,7 +559,7 @@ class LocalMediaPlayer : KoinComponent {
resetMediaPlayer() resetMediaPlayer()
try { try {
setPlayerState(PlayerState.IDLE) setPlayerState(PlayerState.IDLE, currentPlaying)
mediaPlayer.setOnErrorListener(null) mediaPlayer.setOnErrorListener(null)
mediaPlayer.setOnCompletionListener(null) mediaPlayer.setOnCompletionListener(null)
} catch (x: Exception) { } catch (x: Exception) {
@ -603,7 +588,7 @@ class LocalMediaPlayer : KoinComponent {
private val partialFile: File = downloadFile.partialFile private val partialFile: File = downloadFile.partialFile
override fun execute() { override fun execute() {
setPlayerState(PlayerState.DOWNLOADING) setPlayerState(PlayerState.DOWNLOADING, downloadFile)
while (!bufferComplete() && !isOffline()) { while (!bufferComplete() && !isOffline()) {
Util.sleepQuietly(1000L) Util.sleepQuietly(1000L)
if (isCancelled) { if (isCancelled) {
@ -701,11 +686,13 @@ class LocalMediaPlayer : KoinComponent {
// Stop checking position before the song reaches completion // Stop checking position before the song reaches completion
while (isRunning) { while (isRunning) {
try { try {
if (playerState === PlayerState.STARTED) {
synchronized(playerState) {
if (playerState === PlayerState.STARTED) { if (playerState === PlayerState.STARTED) {
cachedPosition = mediaPlayer.currentPosition cachedPosition = mediaPlayer.currentPosition
mediaSessionHandler.updateMediaSessionPlaybackPosition( }
cachedPosition.toLong() }
) RxBus.playbackPositionPublisher.onNext(cachedPosition)
} }
Util.sleepQuietly(100L) Util.sleepQuietly(100L)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -401,7 +401,8 @@ class MediaPlayerController(
get() = localMediaPlayer.playerState get() = localMediaPlayer.playerState
set(state) { set(state) {
val mediaPlayerService = runningInstance val mediaPlayerService = runningInstance
if (mediaPlayerService != null) localMediaPlayer.setPlayerState(state) if (mediaPlayerService != null)
localMediaPlayer.setPlayerState(state, localMediaPlayer.currentPlaying)
} }
@set:Synchronized @set:Synchronized
@ -483,6 +484,7 @@ class MediaPlayerController(
Timber.e(e) Timber.e(e)
} }
}.start() }.start()
// TODO this would be better handled with a Rx command
updateNotification() 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.domain.PlayerState
import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.CacheCleaner
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.MediaSessionEventDistributor
import org.moire.ultrasonic.util.MediaSessionEventListener
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import timber.log.Timber import timber.log.Timber
@ -35,7 +33,6 @@ class MediaPlayerLifecycleSupport : KoinComponent {
private val playbackStateSerializer by inject<PlaybackStateSerializer>() private val playbackStateSerializer by inject<PlaybackStateSerializer>()
private val mediaPlayerController by inject<MediaPlayerController>() private val mediaPlayerController by inject<MediaPlayerController>()
private val downloader by inject<Downloader>() private val downloader by inject<Downloader>()
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
private var created = false private var created = false
private var headsetEventReceiver: BroadcastReceiver? = null private var headsetEventReceiver: BroadcastReceiver? = null

View File

@ -22,7 +22,7 @@ import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent import android.view.KeyEvent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -38,10 +38,7 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4 import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.MediaSessionEventDistributor
import org.moire.ultrasonic.util.MediaSessionEventListener
import org.moire.ultrasonic.util.MediaSessionHandler import org.moire.ultrasonic.util.MediaSessionHandler
import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.ShufflePlayBuffer import org.moire.ultrasonic.util.ShufflePlayBuffer
import org.moire.ultrasonic.util.SimpleServiceBinder import org.moire.ultrasonic.util.SimpleServiceBinder
@ -65,19 +62,13 @@ class MediaPlayerService : Service() {
private val shufflePlayBuffer by inject<ShufflePlayBuffer>() private val shufflePlayBuffer by inject<ShufflePlayBuffer>()
private val downloader by inject<Downloader>() private val downloader by inject<Downloader>()
private val localMediaPlayer by inject<LocalMediaPlayer>() private val localMediaPlayer by inject<LocalMediaPlayer>()
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
private val mediaSessionHandler by inject<MediaSessionHandler>() private val mediaSessionHandler by inject<MediaSessionHandler>()
private var mediaSession: MediaSessionCompat? = null private var mediaSession: MediaSessionCompat? = null
private var mediaSessionToken: MediaSessionCompat.Token? = null private var mediaSessionToken: MediaSessionCompat.Token? = null
private var isInForeground = false private var isInForeground = false
private var notificationBuilder: NotificationCompat.Builder? = null private var notificationBuilder: NotificationCompat.Builder? = null
private lateinit var mediaSessionEventListener: MediaSessionEventListener private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
private var mediaSessionTokenSubscription: Disposable? = null
private val repeatMode: RepeatMode
get() = Settings.repeatMode
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
return binder return binder
@ -89,8 +80,6 @@ class MediaPlayerService : Service() {
shufflePlayBuffer.onCreate() shufflePlayBuffer.onCreate()
localMediaPlayer.init() localMediaPlayer.init()
setupOnCurrentPlayingChangedHandler()
setupOnPlayerStateChangedHandler()
setupOnSongCompletedHandler() setupOnSongCompletedHandler()
localMediaPlayer.onPrepared = { localMediaPlayer.onPrepared = {
@ -104,25 +93,32 @@ class MediaPlayerService : Service() {
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() } 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 // Create Notification Channel
createNotificationChannel() createNotificationChannel()
// Update notification early. It is better to show an empty one temporarily // Update notification early. It is better to show an empty one temporarily
// than waiting too long and letting Android kill the app // than waiting too long and letting Android kill the app
updateNotification(PlayerState.IDLE, null) 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 instance = this
Timber.i("MediaPlayerService created") Timber.i("MediaPlayerService created")
} }
@ -136,9 +132,8 @@ class MediaPlayerService : Service() {
super.onDestroy() super.onDestroy()
instance = null instance = null
try { try {
mediaSessionTokenSubscription?.dispose()
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
mediaSessionHandler.release() mediaSessionHandler.release()
rxBusSubscription.dispose()
localMediaPlayer.release() localMediaPlayer.release()
downloader.stop() downloader.stop()
@ -211,9 +206,7 @@ class MediaPlayerService : Service() {
@Synchronized @Synchronized
fun setNextPlaying() { fun setNextPlaying() {
val gaplessPlayback = Settings.gaplessPlayback if (!Settings.gaplessPlayback) {
if (!gaplessPlayback) {
localMediaPlayer.clearNextPlaying(true) localMediaPlayer.clearNextPlaying(true)
return return
} }
@ -221,7 +214,7 @@ class MediaPlayerService : Service() {
var index = downloader.currentPlayingIndex var index = downloader.currentPlayingIndex
if (index != -1) { if (index != -1) {
when (repeatMode) { when (Settings.repeatMode) {
RepeatMode.OFF -> index += 1 RepeatMode.OFF -> index += 1
RepeatMode.ALL -> index = (index + 1) % downloader.playlist.size RepeatMode.ALL -> index = (index + 1) % downloader.playlist.size
RepeatMode.SINGLE -> { RepeatMode.SINGLE -> {
@ -293,7 +286,6 @@ class MediaPlayerService : Service() {
if (start) { if (start) {
if (jukeboxMediaPlayer.isEnabled) { if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.skip(index, 0) jukeboxMediaPlayer.skip(index, 0)
localMediaPlayer.setPlayerState(PlayerState.STARTED)
} else { } else {
localMediaPlayer.play(downloader.playlist[index]) localMediaPlayer.play(downloader.playlist[index])
} }
@ -321,7 +313,7 @@ class MediaPlayerService : Service() {
} else { } else {
localMediaPlayer.pause() localMediaPlayer.pause()
} }
localMediaPlayer.setPlayerState(PlayerState.PAUSED) localMediaPlayer.setPlayerState(PlayerState.PAUSED, localMediaPlayer.currentPlaying)
} }
} }
@ -334,7 +326,7 @@ class MediaPlayerService : Service() {
localMediaPlayer.pause() localMediaPlayer.pause()
} }
} }
localMediaPlayer.setPlayerState(PlayerState.STOPPED) localMediaPlayer.setPlayerState(PlayerState.STOPPED, null)
} }
@Synchronized @Synchronized
@ -344,7 +336,7 @@ class MediaPlayerService : Service() {
} else { } else {
localMediaPlayer.start() localMediaPlayer.start()
} }
localMediaPlayer.setPlayerState(PlayerState.STARTED) localMediaPlayer.setPlayerState(PlayerState.STARTED, localMediaPlayer.currentPlaying)
} }
private fun updateWidget(playerState: PlayerState, song: MusicDirectory.Entry?) { private fun updateWidget(playerState: PlayerState, song: MusicDirectory.Entry?) {
@ -357,9 +349,10 @@ class MediaPlayerService : Service() {
UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false) UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false)
} }
private fun setupOnCurrentPlayingChangedHandler() { private fun currentPlayingChangedHandler(
localMediaPlayer.onCurrentPlayingChanged = { currentPlaying: DownloadFile? -> playerState: PlayerState,
currentPlaying: DownloadFile?
) {
Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song) Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song)
Util.broadcastA2dpMetaDataChange( Util.broadcastA2dpMetaDataChange(
this@MediaPlayerService, playerPosition, currentPlaying, this@MediaPlayerService, playerPosition, currentPlaying,
@ -367,39 +360,28 @@ class MediaPlayerService : Service() {
) )
// Update widget // Update widget
val playerState = localMediaPlayer.playerState
val song = currentPlaying?.song val song = currentPlaying?.song
updateWidget(playerState, song) updateWidget(playerState, song)
if (currentPlaying != null) { if (currentPlaying != null) {
updateNotification(localMediaPlayer.playerState, currentPlaying) updateNotification(playerState, currentPlaying)
nowPlayingEventDistributor.raiseShowNowPlayingEvent()
} else { } else {
nowPlayingEventDistributor.raiseHideNowPlayingEvent()
stopForeground(true) stopForeground(true)
isInForeground = false isInForeground = false
stopIfIdle() stopIfIdle()
} }
null
} Timber.d("Processed currently playing track change")
} }
private fun setupOnPlayerStateChangedHandler() { private fun playerStateChangedHandler(
localMediaPlayer.onPlayerStateChanged = {
playerState: PlayerState, playerState: PlayerState,
currentPlaying: DownloadFile? currentPlaying: DownloadFile?
-> ) {
val context = this@MediaPlayerService val context = this@MediaPlayerService
// Notify MediaSession
mediaSessionHandler.updateMediaSession(
currentPlaying,
downloader.currentPlayingIndex.toLong(),
playerState
)
if (playerState === PlayerState.PAUSED) { if (playerState === PlayerState.PAUSED) {
playbackStateSerializer.serialize( playbackStateSerializer.serialize(
downloader.playlist, downloader.currentPlayingIndex, playerPosition downloader.playlist, downloader.currentPlayingIndex, playerPosition
@ -426,10 +408,8 @@ class MediaPlayerService : Service() {
// Only update notification if player state is one that will change the icon // Only update notification if player state is one that will change the icon
if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) { if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) {
updateNotification(playerState, currentPlaying) updateNotification(playerState, currentPlaying)
nowPlayingEventDistributor.raiseShowNowPlayingEvent()
} }
} else { } else {
nowPlayingEventDistributor.raiseHideNowPlayingEvent()
stopForeground(true) stopForeground(true)
isInForeground = false isInForeground = false
stopIfIdle() stopIfIdle()
@ -440,9 +420,7 @@ class MediaPlayerService : Service() {
} else if (playerState === PlayerState.COMPLETED) { } else if (playerState === PlayerState.COMPLETED) {
scrobbler.scrobble(currentPlaying, true) scrobbler.scrobble(currentPlaying, true)
} }
Timber.d("Processed player state change")
null
}
} }
private fun setupOnSongCompletedHandler() { private fun setupOnSongCompletedHandler() {
@ -460,7 +438,7 @@ class MediaPlayerService : Service() {
} }
} }
if (index != -1) { if (index != -1) {
when (repeatMode) { when (Settings.repeatMode) {
RepeatMode.OFF -> { RepeatMode.OFF -> {
if (index + 1 < 0 || index + 1 >= downloader.playlist.size) { if (index + 1 < 0 || index + 1 >= downloader.playlist.size) {
if (Settings.shouldClearPlaylist) { if (Settings.shouldClearPlaylist) {

View File

@ -19,7 +19,6 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.MediaSessionHandler
import timber.log.Timber import timber.log.Timber
/** /**
@ -30,9 +29,8 @@ import timber.log.Timber
class PlaybackStateSerializer : KoinComponent { class PlaybackStateSerializer : KoinComponent {
private val context by inject<Context>() 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 setup = AtomicBoolean(false)
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -76,9 +74,6 @@ class PlaybackStateSerializer : KoinComponent {
) )
FileUtil.serialize(context, state, Constants.FILENAME_PLAYLIST_SER) 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?) { fun deserialize(afterDeserialized: (State?) -> Unit?) {
@ -106,7 +101,6 @@ class PlaybackStateSerializer : KoinComponent {
state.currentPlayingPosition state.currentPlayingPosition
) )
mediaSessionHandler.updateMediaSessionQueue(state.songs)
afterDeserialized(state) afterDeserialized(state)
} }
} }

View File

@ -1,42 +1,93 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.os.Bundle
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent import android.view.KeyEvent
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.observables.ConnectableObservable import io.reactivex.rxjava3.disposables.CompositeDisposable
import timber.log.Timber 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 { class RxBus {
companion object {
var mediaSessionTokenPublisher: PublishSubject<MediaSessionCompat.Token> = var mediaSessionTokenPublisher: PublishSubject<MediaSessionCompat.Token> =
PublishSubject.create() PublishSubject.create()
val mediaSessionTokenObservable: Observable<MediaSessionCompat.Token> = val mediaSessionTokenObservable: Observable<MediaSessionCompat.Token> =
mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread()) mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread())
.replay(1) .replay(1)
.autoConnect() .autoConnect(0)
.doOnEach { Timber.d("RxBus mediaSessionTokenPublisher onEach $it")}
val mediaButtonEventPublisher: PublishSubject<KeyEvent> = val mediaButtonEventPublisher: PublishSubject<KeyEvent> =
PublishSubject.create() PublishSubject.create()
val mediaButtonEventObservable: Observable<KeyEvent> = val mediaButtonEventObservable: Observable<KeyEvent> =
mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread()) mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread())
.doOnEach { Timber.d("RxBus mediaButtonEventPublisher onEach $it")}
val themeChangedEventPublisher: PublishSubject<Unit> = val themeChangedEventPublisher: PublishSubject<Unit> =
PublishSubject.create() PublishSubject.create()
val themeChangedEventObservable: Observable<Unit> = val themeChangedEventObservable: Observable<Unit> =
themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
.doOnEach { Timber.d("RxBus themeChangedEventPublisher onEach $it")}
val playerStatePublisher: PublishSubject<StateWithTrack> =
PublishSubject.create()
val playerStateObservable: Observable<StateWithTrack> =
playerStatePublisher.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect(0)
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> = val dismissNowPlayingCommandPublisher: PublishSubject<Unit> =
PublishSubject.create() PublishSubject.create()
val dismissNowPlayingCommandObservable: Observable<Unit> = val dismissNowPlayingCommandObservable: Observable<Unit> =
dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread())
.doOnEach { Timber.d("RxBus dismissNowPlayingCommandPublisher onEach $it")}
fun releaseMediaSessionToken() { mediaSessionTokenPublisher = PublishSubject.create() } 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
import android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN import android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN
import android.view.KeyEvent import android.view.KeyEvent
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlin.Pair
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.imageloader.BitmapUtils import org.moire.ultrasonic.imageloader.BitmapUtils
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
import timber.log.Timber import timber.log.Timber
private const val INTENT_CODE_MEDIA_BUTTON = 161 private const val INTENT_CODE_MEDIA_BUTTON = 161
private const val CALL_DIVIDE = 10
/** /**
* Central place to handle the state of the MediaSession * Central place to handle the state of the MediaSession
*/ */
@ -40,14 +41,14 @@ class MediaSessionHandler : KoinComponent {
private var playbackActions: Long? = null private var playbackActions: Long? = null
private var cachedPlayingIndex: Long? = null private var cachedPlayingIndex: Long? = null
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
private val applicationContext by inject<Context>() private val applicationContext by inject<Context>()
private var referenceCount: Int = 0 private var referenceCount: Int = 0
private var cachedPlaylist: List<MediaSessionCompat.QueueItem>? = null private var cachedPlaylist: List<DownloadFile>? = null
private var playbackPositionDelayCount: Int = 0
private var cachedPosition: Long = 0 private var cachedPosition: Long = 0
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
fun release() { fun release() {
if (referenceCount > 0) referenceCount-- if (referenceCount > 0) referenceCount--
@ -55,6 +56,7 @@ class MediaSessionHandler : KoinComponent {
mediaSession?.isActive = false mediaSession?.isActive = false
RxBus.releaseMediaSessionToken() RxBus.releaseMediaSessionToken()
rxBusSubscription.dispose()
mediaSession?.release() mediaSession?.release()
mediaSession = null mediaSession = null
@ -94,14 +96,14 @@ class MediaSessionHandler : KoinComponent {
super.onPlayFromMediaId(mediaId, extras) super.onPlayFromMediaId(mediaId, extras)
Timber.d("Media Session Callback: onPlayFromMediaId %s", mediaId) 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?) { override fun onPlayFromSearch(query: String?, extras: Bundle?) {
super.onPlayFromSearch(query, extras) super.onPlayFromSearch(query, extras)
Timber.d("Media Session Callback: onPlayFromSearch %s", query) Timber.d("Media Session Callback: onPlayFromSearch %s", query)
mediaSessionEventDistributor.raisePlayFromSearchRequestedEvent(query, extras) RxBus.playFromSearchCommandPublisher.onNext(Pair(query, extras))
} }
override fun onPause() { override fun onPause() {
@ -154,22 +156,30 @@ class MediaSessionHandler : KoinComponent {
override fun onSkipToQueueItem(id: Long) { override fun onSkipToQueueItem(id: Long) {
super.onSkipToQueueItem(id) 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 // It seems to be the best practice to set this to true for the lifetime of the session
mediaSession?.isActive = true 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") Timber.i("MediaSessionHandler.initialize Media Session created")
} }
@Suppress("TooGenericExceptionCaught", "LongMethod") @Suppress("LongMethod", "ComplexMethod")
fun updateMediaSession( private fun updateMediaSession(
currentPlaying: DownloadFile?, playerState: PlayerState,
currentPlayingIndex: Long?, currentPlaying: DownloadFile?
playerState: PlayerState
) { ) {
Timber.d("Updating the MediaSession") 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_ALBUM, song.album)
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title) metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title)
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover) metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover)
} catch (e: Exception) { } catch (all: Exception) {
Timber.e(e, "Error setting the metadata") Timber.e(all, "Error setting the metadata")
} }
} }
@ -245,52 +255,45 @@ class MediaSessionHandler : KoinComponent {
// Set actions // Set actions
playbackStateBuilder.setActions(playbackActions!!) playbackStateBuilder.setActions(playbackActions!!)
cachedPlayingIndex = currentPlayingIndex val index = cachedPlaylist?.indexOf(currentPlaying)
setMediaSessionQueue(cachedPlaylist) cachedPlayingIndex = if (index == null || index < 0) null else index.toLong()
cachedPlaylist?.let { setMediaSessionQueue(it) }
if ( if (
currentPlayingIndex != null && cachedPlaylist != null && cachedPlayingIndex != null && cachedPlaylist != null &&
!Settings.shouldDisableNowPlayingListSending !Settings.shouldDisableNowPlayingListSending
) )
playbackStateBuilder.setActiveQueueItemId(currentPlayingIndex) cachedPlayingIndex?.let { playbackStateBuilder.setActiveQueueItemId(it) }
// Save the playback state // Save the playback state
mediaSession?.setPlaybackState(playbackStateBuilder.build()) mediaSession?.setPlaybackState(playbackStateBuilder.build())
} }
fun updateMediaSessionQueue(playlist: Iterable<MusicDirectory.Entry>) { private fun updateMediaSessionQueue(playlist: List<DownloadFile>) {
// This call is cached because Downloader may initialize earlier than the MediaSession cachedPlaylist = playlist
cachedPlaylist = playlist.mapIndexed { id, song -> setMediaSessionQueue(playlist)
MediaSessionCompat.QueueItem(
Util.getMediaDescriptionForEntry(song),
id.toLong()
)
}
setMediaSessionQueue(cachedPlaylist)
} }
private fun setMediaSessionQueue(queue: List<MediaSessionCompat.QueueItem>?) { private fun setMediaSessionQueue(playlist: List<DownloadFile>) {
if (mediaSession == null) return if (mediaSession == null) return
if (Settings.shouldDisableNowPlayingListSending) 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?.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
mediaSession?.setQueue(queue) mediaSession?.setQueue(queue)
} }
fun updateMediaSessionPlaybackPosition(playbackPosition: Long) { private fun updateMediaSessionPlaybackPosition(playbackPosition: Int) {
cachedPosition = playbackPosition.toLong()
cachedPosition = playbackPosition
if (mediaSession == null) return
if (playbackState == null || playbackActions == null) return if (playbackState == null || playbackActions == null) return
// Playback position is updated too frequently in the player.
// This counter makes sure that the MediaSession is updated ~ at every second
playbackPositionDelayCount++
if (playbackPositionDelayCount < CALL_DIVIDE) return
playbackPositionDelayCount = 0
val playbackStateBuilder = PlaybackStateCompat.Builder() val playbackStateBuilder = PlaybackStateCompat.Builder()
playbackStateBuilder.setState(playbackState!!, playbackPosition, 1.0f) playbackStateBuilder.setState(playbackState!!, cachedPosition, 1.0f)
playbackStateBuilder.setActions(playbackActions!!) playbackStateBuilder.setActions(playbackActions!!)
if ( 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()
}