parent
fec2d78d30
commit
ffb2d59886
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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?) {}
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package org.moire.ultrasonic.util
|
||||
|
||||
/**
|
||||
* Callback interface for Now Playing event subscribers
|
||||
*/
|
||||
interface NowPlayingEventListener {
|
||||
fun onHideNowPlaying()
|
||||
fun onShowNowPlaying()
|
||||
}
|
Loading…
Reference in New Issue