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.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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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,76 +89,24 @@ 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?) {
|
}
|
||||||
Timber.d(
|
|
||||||
"AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s",
|
rxBusSubscription += RxBus.playFromSearchCommandObservable.subscribe {
|
||||||
mediaId
|
playFromSearchCommand(it.first)
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
|
||||||
mediaSessionHandler.initialize()
|
mediaSessionHandler.initialize()
|
||||||
|
|
||||||
val handler = Handler()
|
val handler = Handler()
|
||||||
|
@ -182,10 +125,66 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||||
Timber.i("AutoMediaBrowserService onCreate finished")
|
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() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
mediaSessionTokenSubscription?.dispose()
|
rxBusSubscription.dispose()
|
||||||
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
|
||||||
mediaSessionHandler.release()
|
mediaSessionHandler.release()
|
||||||
serviceJob.cancel()
|
serviceJob.cancel()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
this.playerState = playerState
|
synchronized(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) {
|
||||||
|
@ -702,10 +687,12 @@ class LocalMediaPlayer : KoinComponent {
|
||||||
while (isRunning) {
|
while (isRunning) {
|
||||||
try {
|
try {
|
||||||
if (playerState === PlayerState.STARTED) {
|
if (playerState === PlayerState.STARTED) {
|
||||||
cachedPosition = mediaPlayer.currentPosition
|
synchronized(playerState) {
|
||||||
mediaSessionHandler.updateMediaSessionPlaybackPosition(
|
if (playerState === PlayerState.STARTED) {
|
||||||
cachedPosition.toLong()
|
cachedPosition = mediaPlayer.currentPosition
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
RxBus.playbackPositionPublisher.onNext(cachedPosition)
|
||||||
}
|
}
|
||||||
Util.sleepQuietly(100L)
|
Util.sleepQuietly(100L)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,92 +349,78 @@ 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.broadcastA2dpMetaDataChange(
|
||||||
|
this@MediaPlayerService, playerPosition, currentPlaying,
|
||||||
|
downloader.all.size, downloader.currentPlayingIndex + 1
|
||||||
|
)
|
||||||
|
|
||||||
Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song)
|
// Update widget
|
||||||
Util.broadcastA2dpMetaDataChange(
|
val song = currentPlaying?.song
|
||||||
this@MediaPlayerService, playerPosition, currentPlaying,
|
|
||||||
downloader.all.size, downloader.currentPlayingIndex + 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update widget
|
updateWidget(playerState, song)
|
||||||
val playerState = localMediaPlayer.playerState
|
|
||||||
val song = currentPlaying?.song
|
|
||||||
|
|
||||||
updateWidget(playerState, song)
|
if (currentPlaying != null) {
|
||||||
|
updateNotification(playerState, currentPlaying)
|
||||||
if (currentPlaying != null) {
|
} else {
|
||||||
updateNotification(localMediaPlayer.playerState, currentPlaying)
|
stopForeground(true)
|
||||||
nowPlayingEventDistributor.raiseShowNowPlayingEvent()
|
isInForeground = false
|
||||||
} else {
|
stopIfIdle()
|
||||||
nowPlayingEventDistributor.raiseHideNowPlayingEvent()
|
|
||||||
stopForeground(true)
|
|
||||||
isInForeground = false
|
|
||||||
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
|
if (playerState === PlayerState.PAUSED) {
|
||||||
mediaSessionHandler.updateMediaSession(
|
playbackStateSerializer.serialize(
|
||||||
currentPlaying,
|
downloader.playlist, downloader.currentPlayingIndex, playerPosition
|
||||||
downloader.currentPlayingIndex.toLong(),
|
|
||||||
playerState
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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() {
|
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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
var mediaSessionTokenPublisher: PublishSubject<MediaSessionCompat.Token> =
|
companion object {
|
||||||
PublishSubject.create()
|
var mediaSessionTokenPublisher: PublishSubject<MediaSessionCompat.Token> =
|
||||||
val mediaSessionTokenObservable: Observable<MediaSessionCompat.Token> =
|
PublishSubject.create()
|
||||||
mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread())
|
val mediaSessionTokenObservable: Observable<MediaSessionCompat.Token> =
|
||||||
.replay(1)
|
mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoConnect()
|
.replay(1)
|
||||||
.doOnEach { Timber.d("RxBus mediaSessionTokenPublisher onEach $it")}
|
.autoConnect(0)
|
||||||
|
|
||||||
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 dismissNowPlayingCommandPublisher: PublishSubject<Unit> =
|
val playerStatePublisher: PublishSubject<StateWithTrack> =
|
||||||
PublishSubject.create()
|
PublishSubject.create()
|
||||||
val dismissNowPlayingCommandObservable: Observable<Unit> =
|
val playerStateObservable: Observable<StateWithTrack> =
|
||||||
dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread())
|
playerStatePublisher.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnEach { Timber.d("RxBus dismissNowPlayingCommandPublisher onEach $it")}
|
.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
|
||||||
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 (
|
||||||
|
|
|
@ -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