mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-16 11:41:16 +01:00
commit
97eb753413
@ -43,6 +43,8 @@ ext.versions = [
|
||||
timber : "4.7.1",
|
||||
fastScroll : "2.0.1",
|
||||
colorPicker : "2.2.3",
|
||||
rxJava : "3.1.2",
|
||||
rxAndroid : "3.0.0",
|
||||
]
|
||||
|
||||
ext.gradlePlugins = [
|
||||
@ -91,6 +93,8 @@ ext.other = [
|
||||
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
|
||||
sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView",
|
||||
colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker",
|
||||
rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava",
|
||||
rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid",
|
||||
]
|
||||
|
||||
ext.testing = [
|
||||
|
@ -106,6 +106,8 @@ dependencies {
|
||||
implementation other.fastScroll
|
||||
implementation other.sortListView
|
||||
implementation other.colorPickerView
|
||||
implementation other.rxJava
|
||||
implementation other.rxAndroid
|
||||
|
||||
kapt androidSupport.room
|
||||
|
||||
|
@ -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.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 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 onDismissNowPlaying() { }
|
||||
@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)
|
||||
{
|
||||
nowPlayingEventDistributor.getValue().raiseNowPlayingDismissedEvent();
|
||||
return false;
|
||||
}
|
||||
if (deltaY > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(R.id.playerFragment);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -31,6 +31,7 @@ import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
@ -43,17 +44,14 @@ import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||
import org.moire.ultrasonic.service.DownloadFile
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
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
|
||||
import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler
|
||||
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
||||
import org.moire.ultrasonic.util.ThemeChangedEventListener
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
||||
@ -74,15 +72,13 @@ class NavigationActivity : AppCompatActivity() {
|
||||
private var headerBackgroundImage: ImageView? = null
|
||||
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
private lateinit var nowPlayingEventListener: NowPlayingEventListener
|
||||
private lateinit var themeChangedEventListener: ThemeChangedEventListener
|
||||
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 themeChangedEventDistributor: ThemeChangedEventDistributor by inject()
|
||||
private val permissionUtil: PermissionUtil by inject()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
private val serverRepository: ServerSettingDao by inject()
|
||||
@ -169,28 +165,22 @@ class NavigationActivity : AppCompatActivity() {
|
||||
showWelcomeDialog()
|
||||
}
|
||||
|
||||
nowPlayingEventListener = object : NowPlayingEventListener {
|
||||
override fun onDismissNowPlaying() {
|
||||
nowPlayingHidden = true
|
||||
hideNowPlaying()
|
||||
}
|
||||
RxBus.dismissNowPlayingCommandObservable.subscribe {
|
||||
nowPlayingHidden = true
|
||||
hideNowPlaying()
|
||||
}
|
||||
|
||||
override fun onHideNowPlaying() {
|
||||
hideNowPlaying()
|
||||
}
|
||||
|
||||
override fun onShowNowPlaying() {
|
||||
playerStateSubscription = RxBus.playerStateObservable.subscribe {
|
||||
if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED)
|
||||
showNowPlaying()
|
||||
}
|
||||
else
|
||||
hideNowPlaying()
|
||||
}
|
||||
|
||||
themeChangedEventListener = object : ThemeChangedEventListener {
|
||||
override fun onThemeChanged() { recreate() }
|
||||
themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe {
|
||||
recreate()
|
||||
}
|
||||
|
||||
nowPlayingEventDistributor.subscribe(nowPlayingEventListener)
|
||||
themeChangedEventDistributor.subscribe(themeChangedEventListener)
|
||||
|
||||
serverRepository.liveServerCount().observe(
|
||||
this,
|
||||
{ count ->
|
||||
@ -237,8 +227,8 @@ class NavigationActivity : AppCompatActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
|
||||
themeChangedEventDistributor.unsubscribe(themeChangedEventListener)
|
||||
themeChangedEventSubscription?.dispose()
|
||||
playerStateSubscription?.dispose()
|
||||
imageLoaderProvider.clearImageLoader()
|
||||
permissionUtil.onForegroundApplicationStopped()
|
||||
}
|
||||
|
@ -4,11 +4,8 @@ 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
|
||||
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
||||
|
||||
/**
|
||||
* This Koin module contains the registration of general classes needed for Ultrasonic
|
||||
@ -17,8 +14,5 @@ val applicationModule = module {
|
||||
single { ActiveServerProvider(get()) }
|
||||
single { ImageLoaderProvider(androidContext()) }
|
||||
single { PermissionUtil(androidContext()) }
|
||||
single { NowPlayingEventDistributor() }
|
||||
single { ThemeChangedEventDistributor() }
|
||||
single { MediaSessionEventDistributor() }
|
||||
single { MediaSessionHandler() }
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package org.moire.ultrasonic.domain
|
||||
import java.text.SimpleDateFormat
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import org.moire.ultrasonic.api.subsonic.models.Playlist as APIPlaylist
|
||||
import org.moire.ultrasonic.util.Util.ifNotNull
|
||||
|
||||
internal val playlistDateFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
|
||||
|
||||
@ -17,7 +18,7 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory(
|
||||
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
||||
this.id, this.name, this.owner,
|
||||
this.comment, this.songCount.toString(),
|
||||
this.created?.let { playlistDateFormat.format(it.time) } ?: "",
|
||||
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
|
||||
public
|
||||
)
|
||||
|
||||
|
@ -5,6 +5,7 @@ package org.moire.ultrasonic.domain
|
||||
import java.text.SimpleDateFormat
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import org.moire.ultrasonic.api.subsonic.models.Share as APIShare
|
||||
import org.moire.ultrasonic.util.Util.ifNotNull
|
||||
|
||||
internal val shareTimeFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
|
||||
|
||||
@ -13,11 +14,11 @@ fun List<APIShare>.toDomainEntitiesList(): List<Share> = this.map {
|
||||
}
|
||||
|
||||
fun APIShare.toDomainEntity(): Share = Share(
|
||||
created = this@toDomainEntity.created?.let { shareTimeFormat.format(it.time) },
|
||||
created = this@toDomainEntity.created.ifNotNull { shareTimeFormat.format(it.time) },
|
||||
description = this@toDomainEntity.description,
|
||||
expires = this@toDomainEntity.expires?.let { shareTimeFormat.format(it.time) },
|
||||
expires = this@toDomainEntity.expires.ifNotNull { shareTimeFormat.format(it.time) },
|
||||
id = this@toDomainEntity.id,
|
||||
lastVisited = this@toDomainEntity.lastVisited?.let { shareTimeFormat.format(it.time) },
|
||||
lastVisited = this@toDomainEntity.lastVisited.ifNotNull { shareTimeFormat.format(it.time) },
|
||||
url = this@toDomainEntity.url,
|
||||
username = this@toDomainEntity.username,
|
||||
visitCount = this@toDomainEntity.visitCount.toLong(),
|
||||
|
@ -220,6 +220,6 @@ class DownloadListModel(application: Application) : GenericListModel(application
|
||||
private val downloader by inject<Downloader>()
|
||||
|
||||
fun getList(): LiveData<List<DownloadFile>> {
|
||||
return downloader.observableList
|
||||
return downloader.observableDownloads
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -35,9 +35,11 @@ import android.widget.TextView
|
||||
import android.widget.ViewFlipper
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.Navigation
|
||||
import com.mobeta.android.dslv.DragSortListView
|
||||
import com.mobeta.android.dslv.DragSortListView.DragSortListener
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.ArrayList
|
||||
@ -49,6 +51,7 @@ import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
@ -66,6 +69,7 @@ import org.moire.ultrasonic.service.DownloadFile
|
||||
import org.moire.ultrasonic.service.LocalMediaPlayer
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||
@ -88,8 +92,6 @@ import timber.log.Timber
|
||||
*/
|
||||
@Suppress("LargeClass", "TooManyFunctions", "MagicNumber")
|
||||
class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinComponent {
|
||||
// Settings
|
||||
private var currentRevision: Long = 0
|
||||
private var swipeDistance = 0
|
||||
private var swipeVelocity = 0
|
||||
private var jukeboxAvailable = false
|
||||
@ -111,6 +113,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
||||
private var currentPlaying: DownloadFile? = null
|
||||
private var currentSong: MusicDirectory.Entry? = null
|
||||
private var onProgressChangedTask: SilentBackgroundTask<Void?>? = null
|
||||
private var rxBusSubscription: Disposable? = null
|
||||
|
||||
// Views and UI Elements
|
||||
private lateinit var visualizerViewLayout: LinearLayout
|
||||
@ -419,13 +422,21 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
||||
}
|
||||
}
|
||||
)
|
||||
Thread {
|
||||
|
||||
// Observe playlist changes and update the UI
|
||||
rxBusSubscription = RxBus.playlistObservable.subscribe {
|
||||
onPlaylistChanged()
|
||||
}
|
||||
|
||||
// Query the Jukebox state off-thread
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
jukeboxAvailable = mediaPlayerController.isJukeboxAvailable
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) }
|
||||
}
|
||||
|
||||
@ -479,6 +490,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
rxBusSubscription?.dispose()
|
||||
cancellationToken.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
@ -797,9 +809,6 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
||||
private fun update(cancel: CancellationToken?) {
|
||||
if (cancel!!.isCancellationRequested) return
|
||||
val mediaPlayerController = mediaPlayerController
|
||||
if (currentRevision != mediaPlayerController.playListUpdateRevision) {
|
||||
onPlaylistChanged()
|
||||
}
|
||||
if (currentPlaying != mediaPlayerController.currentPlaying) {
|
||||
onCurrentChanged()
|
||||
}
|
||||
@ -914,7 +923,6 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
||||
|
||||
emptyTextView.isVisible = list.isEmpty()
|
||||
|
||||
currentRevision = mediaPlayerController.playListUpdateRevision
|
||||
when (mediaPlayerController.repeatMode) {
|
||||
RepeatMode.OFF -> repeatButton.setImageDrawable(
|
||||
Util.getDrawableFromAttribute(
|
||||
|
@ -35,6 +35,7 @@ import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest
|
||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest
|
||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.FileUtil.defaultMusicDirectory
|
||||
import org.moire.ultrasonic.util.FileUtil.ensureDirectoryExistsAndIsReadWritable
|
||||
@ -46,7 +47,6 @@ import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Settings.preferences
|
||||
import org.moire.ultrasonic.util.Settings.shareGreeting
|
||||
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
|
||||
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
||||
import org.moire.ultrasonic.util.TimeSpanPreference
|
||||
import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
@ -92,12 +92,11 @@ class SettingsFragment :
|
||||
private val mediaPlayerControllerLazy = inject<MediaPlayerController>(
|
||||
MediaPlayerController::class.java
|
||||
)
|
||||
|
||||
private val permissionUtil = inject<PermissionUtil>(
|
||||
PermissionUtil::class.java
|
||||
)
|
||||
private val themeChangedEventDistributor = inject<ThemeChangedEventDistributor>(
|
||||
ThemeChangedEventDistributor::class.java
|
||||
)
|
||||
|
||||
private val mediaSessionHandler = inject<MediaSessionHandler>(
|
||||
MediaSessionHandler::class.java
|
||||
)
|
||||
@ -201,7 +200,7 @@ class SettingsFragment :
|
||||
showArtistPicture!!.isEnabled = sharedPreferences.getBoolean(key, false)
|
||||
}
|
||||
Constants.PREFERENCES_KEY_THEME -> {
|
||||
themeChangedEventDistributor.value.RaiseThemeChangedEvent()
|
||||
RxBus.themeChangedEventPublisher.onNext(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ class TrackCollectionFragment : Fragment() {
|
||||
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
context?.let { CommunicationErrorHandler.handleError(exception, it) }
|
||||
CommunicationErrorHandler.handleError(exception, context)
|
||||
}
|
||||
refreshAlbumListView!!.isRefreshing = false
|
||||
}
|
||||
|
@ -11,9 +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.CompositeDisposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -25,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
|
||||
@ -73,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>()
|
||||
@ -93,75 +89,24 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
private val useId3Tags get() = Settings.shouldUseId3Tags
|
||||
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
||||
|
||||
private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
mediaSessionEventListener = object : MediaSessionEventListener {
|
||||
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
|
||||
if (sessionToken == null) {
|
||||
sessionToken = token
|
||||
}
|
||||
}
|
||||
|
||||
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.mediaSessionTokenObservable.subscribe {
|
||||
if (sessionToken == null) sessionToken = it
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.playFromMediaIdCommandObservable.subscribe {
|
||||
playFromMediaId(it.first)
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.playFromSearchCommandObservable.subscribe {
|
||||
playFromSearchCommand(it.first)
|
||||
}
|
||||
|
||||
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
||||
mediaSessionHandler.initialize()
|
||||
|
||||
val handler = Handler()
|
||||
@ -180,9 +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()
|
||||
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
||||
rxBusSubscription.dispose()
|
||||
mediaSessionHandler.release()
|
||||
serviceJob.cancel()
|
||||
|
||||
|
@ -39,9 +39,11 @@ import timber.log.Timber
|
||||
*/
|
||||
class CommunicationErrorHandler {
|
||||
companion object {
|
||||
fun handleError(error: Throwable?, context: Context) {
|
||||
fun handleError(error: Throwable?, context: Context?) {
|
||||
Timber.w(error)
|
||||
|
||||
if (context == null) return
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setTitle(R.string.error_label)
|
||||
|
@ -30,13 +30,17 @@ class Downloader(
|
||||
private val externalStorageMonitor: ExternalStorageMonitor,
|
||||
private val localMediaPlayer: LocalMediaPlayer
|
||||
) : KoinComponent {
|
||||
val playlist: MutableList<DownloadFile> = ArrayList()
|
||||
|
||||
private val playlist = mutableListOf<DownloadFile>()
|
||||
|
||||
var started: Boolean = false
|
||||
|
||||
private val downloadQueue: PriorityQueue<DownloadFile> = PriorityQueue<DownloadFile>()
|
||||
private val activelyDownloading: MutableList<DownloadFile> = ArrayList()
|
||||
private val downloadQueue = PriorityQueue<DownloadFile>()
|
||||
private val activelyDownloading = mutableListOf<DownloadFile>()
|
||||
|
||||
val observableList: MutableLiveData<List<DownloadFile>> = MutableLiveData<List<DownloadFile>>()
|
||||
// TODO: The playlist is now published with RX, while the observableDownloads is using LiveData.
|
||||
// Use the same for both
|
||||
val observableDownloads = MutableLiveData<List<DownloadFile>>()
|
||||
|
||||
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
|
||||
|
||||
@ -45,8 +49,11 @@ class Downloader(
|
||||
private var executorService: ScheduledExecutorService? = null
|
||||
private var wifiLock: WifiManager.WifiLock? = null
|
||||
|
||||
var playlistUpdateRevision: Long = 0
|
||||
private set
|
||||
private var playlistUpdateRevision: Long = 0
|
||||
private set(value) {
|
||||
field = value
|
||||
RxBus.playlistPublisher.onNext(playlist)
|
||||
}
|
||||
|
||||
val downloadChecker = Runnable {
|
||||
try {
|
||||
@ -61,7 +68,7 @@ class Downloader(
|
||||
stop()
|
||||
clearPlaylist()
|
||||
clearBackground()
|
||||
observableList.value = listOf()
|
||||
observableDownloads.value = listOf()
|
||||
Timber.i("Downloader destroyed")
|
||||
}
|
||||
|
||||
@ -179,7 +186,7 @@ class Downloader(
|
||||
}
|
||||
|
||||
private fun updateLiveData() {
|
||||
observableList.postValue(downloads)
|
||||
observableDownloads.postValue(downloads)
|
||||
}
|
||||
|
||||
private fun startDownloadOnService(task: DownloadFile) {
|
||||
@ -264,6 +271,10 @@ class Downloader(
|
||||
return temp.distinct().sorted()
|
||||
}
|
||||
|
||||
// Public facing playlist (immutable)
|
||||
@Synchronized
|
||||
fun getPlaylist(): List<DownloadFile> = playlist
|
||||
|
||||
@Synchronized
|
||||
fun clearPlaylist() {
|
||||
playlist.clear()
|
||||
@ -349,6 +360,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 +454,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.playerStatePublisher.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) {
|
||||
|
@ -180,7 +180,7 @@ class MediaPlayerController(
|
||||
downloader.addToPlaylist(filteredSongs, save, autoPlay, playNext, newPlaylist)
|
||||
jukeboxMediaPlayer.updatePlaylist()
|
||||
if (shuffle) shuffle()
|
||||
val isLastTrack = (downloader.playlist.size - 1 == downloader.currentPlayingIndex)
|
||||
val isLastTrack = (downloader.getPlaylist().size - 1 == downloader.currentPlayingIndex)
|
||||
|
||||
if (!playNext && !autoPlay && isLastTrack) {
|
||||
val mediaPlayerService = runningInstance
|
||||
@ -190,15 +190,15 @@ class MediaPlayerController(
|
||||
if (autoPlay) {
|
||||
play(0)
|
||||
} else {
|
||||
if (localMediaPlayer.currentPlaying == null && downloader.playlist.size > 0) {
|
||||
localMediaPlayer.currentPlaying = downloader.playlist[0]
|
||||
downloader.playlist[0].setPlaying(true)
|
||||
if (localMediaPlayer.currentPlaying == null && downloader.getPlaylist().isNotEmpty()) {
|
||||
localMediaPlayer.currentPlaying = downloader.getPlaylist()[0]
|
||||
downloader.getPlaylist()[0].setPlaying(true)
|
||||
}
|
||||
downloader.checkDownloads()
|
||||
}
|
||||
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex,
|
||||
playerPosition
|
||||
)
|
||||
@ -210,7 +210,7 @@ class MediaPlayerController(
|
||||
val filteredSongs = songs.filterNotNull()
|
||||
downloader.downloadBackground(filteredSongs, save)
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex,
|
||||
playerPosition
|
||||
)
|
||||
@ -241,7 +241,7 @@ class MediaPlayerController(
|
||||
fun shuffle() {
|
||||
downloader.shuffle()
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex,
|
||||
playerPosition
|
||||
)
|
||||
@ -270,7 +270,7 @@ class MediaPlayerController(
|
||||
downloader.clearPlaylist()
|
||||
if (serialize) {
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex, playerPosition
|
||||
)
|
||||
}
|
||||
@ -281,16 +281,11 @@ class MediaPlayerController(
|
||||
@Synchronized
|
||||
fun clearIncomplete() {
|
||||
reset()
|
||||
val iterator = downloader.playlist.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val downloadFile = iterator.next()
|
||||
if (!downloadFile.isCompleteFileAvailable) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
downloader.clearIncomplete()
|
||||
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex,
|
||||
playerPosition
|
||||
)
|
||||
@ -307,7 +302,7 @@ class MediaPlayerController(
|
||||
downloader.removeFromPlaylist(downloadFile)
|
||||
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex,
|
||||
playerPosition
|
||||
)
|
||||
@ -359,12 +354,12 @@ class MediaPlayerController(
|
||||
when (repeatMode) {
|
||||
RepeatMode.SINGLE, RepeatMode.OFF -> {
|
||||
// Play next if exists
|
||||
if (index + 1 >= 0 && index + 1 < downloader.playlist.size) {
|
||||
if (index + 1 >= 0 && index + 1 < downloader.getPlaylist().size) {
|
||||
play(index + 1)
|
||||
}
|
||||
}
|
||||
RepeatMode.ALL -> {
|
||||
play((index + 1) % downloader.playlist.size)
|
||||
play((index + 1) % downloader.getPlaylist().size)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
@ -397,7 +392,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
|
||||
@ -479,6 +475,7 @@ class MediaPlayerController(
|
||||
Timber.e(e)
|
||||
}
|
||||
}.start()
|
||||
// TODO this would be better handled with a Rx command
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
@ -490,16 +487,13 @@ class MediaPlayerController(
|
||||
}
|
||||
|
||||
val playlistSize: Int
|
||||
get() = downloader.playlist.size
|
||||
get() = downloader.getPlaylist().size
|
||||
|
||||
val currentPlayingNumberOnPlaylist: Int
|
||||
get() = downloader.currentPlayingIndex
|
||||
|
||||
val playList: List<DownloadFile>
|
||||
get() = downloader.playlist
|
||||
|
||||
val playListUpdateRevision: Long
|
||||
get() = downloader.playlistUpdateRevision
|
||||
get() = downloader.getPlaylist()
|
||||
|
||||
val playListDuration: Long
|
||||
get() = downloader.downloadListDuration
|
||||
|
@ -13,6 +13,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import android.view.KeyEvent
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
@ -20,9 +21,8 @@ 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 org.moire.ultrasonic.util.Util.ifNotNull
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
@ -34,11 +34,10 @@ 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
|
||||
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
||||
private var mediaButtonEventSubscription: Disposable? = null
|
||||
|
||||
fun onCreate() {
|
||||
onCreate(false, null)
|
||||
@ -51,13 +50,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
return
|
||||
}
|
||||
|
||||
mediaSessionEventListener = object : MediaSessionEventListener {
|
||||
override fun onMediaButtonEvent(keyEvent: KeyEvent?) {
|
||||
if (keyEvent != null) handleKeyEvent(keyEvent)
|
||||
}
|
||||
mediaButtonEventSubscription = RxBus.mediaButtonEventObservable.subscribe {
|
||||
handleKeyEvent(it)
|
||||
}
|
||||
|
||||
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
||||
registerHeadsetReceiver()
|
||||
mediaPlayerController.onCreate()
|
||||
if (autoPlay) mediaPlayerController.preload()
|
||||
@ -75,7 +71,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
// Work-around: Serialize again, as the restore() method creates a
|
||||
// serialization without current playing info.
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex,
|
||||
mediaPlayerController.playerPosition
|
||||
)
|
||||
@ -92,14 +88,13 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
if (!created) return
|
||||
|
||||
playbackStateSerializer.serializeNow(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex,
|
||||
mediaPlayerController.playerPosition
|
||||
)
|
||||
|
||||
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
||||
|
||||
mediaPlayerController.clear(false)
|
||||
mediaButtonEventSubscription?.dispose()
|
||||
applicationContext().unregisterReceiver(headsetEventReceiver)
|
||||
mediaPlayerController.onDestroy()
|
||||
|
||||
@ -119,7 +114,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
if (intentAction == Constants.CMD_PROCESS_KEYCODE) {
|
||||
if (intent.extras != null) {
|
||||
val event = intent.extras!![Intent.EXTRA_KEY_EVENT] as KeyEvent?
|
||||
event?.let { handleKeyEvent(it) }
|
||||
event.ifNotNull { handleKeyEvent(it) }
|
||||
}
|
||||
} else {
|
||||
handleUltrasonicIntent(intentAction)
|
||||
|
@ -22,6 +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.CompositeDisposable
|
||||
import kotlin.collections.ArrayList
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.moire.ultrasonic.R
|
||||
@ -37,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
|
||||
@ -64,18 +62,16 @@ 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 rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private val repeatMode: RepeatMode
|
||||
get() = Settings.repeatMode
|
||||
private var currentPlayerState: PlayerState? = null
|
||||
private var currentTrack: DownloadFile? = null
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
@ -87,13 +83,11 @@ class MediaPlayerService : Service() {
|
||||
shufflePlayBuffer.onCreate()
|
||||
localMediaPlayer.init()
|
||||
|
||||
setupOnCurrentPlayingChangedHandler()
|
||||
setupOnPlayerStateChangedHandler()
|
||||
setupOnSongCompletedHandler()
|
||||
|
||||
localMediaPlayer.onPrepared = {
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex,
|
||||
playerPosition
|
||||
)
|
||||
@ -102,25 +96,28 @@ class MediaPlayerService : Service() {
|
||||
|
||||
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
|
||||
|
||||
mediaSessionEventListener = object : MediaSessionEventListener {
|
||||
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
|
||||
mediaSessionToken = token
|
||||
}
|
||||
|
||||
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.mediaSessionTokenObservable.subscribe {
|
||||
mediaSessionToken = it
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.skipToQueueItemCommandObservable.subscribe {
|
||||
play(it.toInt())
|
||||
}
|
||||
|
||||
mediaSessionHandler.initialize()
|
||||
|
||||
instance = this
|
||||
Timber.i("MediaPlayerService created")
|
||||
}
|
||||
@ -134,8 +131,8 @@ class MediaPlayerService : Service() {
|
||||
super.onDestroy()
|
||||
instance = null
|
||||
try {
|
||||
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
||||
mediaSessionHandler.release()
|
||||
rxBusSubscription.dispose()
|
||||
|
||||
localMediaPlayer.release()
|
||||
downloader.stop()
|
||||
@ -201,16 +198,14 @@ class MediaPlayerService : Service() {
|
||||
@Synchronized
|
||||
fun setCurrentPlaying(currentPlayingIndex: Int) {
|
||||
try {
|
||||
localMediaPlayer.setCurrentPlaying(downloader.playlist[currentPlayingIndex])
|
||||
localMediaPlayer.setCurrentPlaying(downloader.getPlaylist()[currentPlayingIndex])
|
||||
} catch (ignored: IndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setNextPlaying() {
|
||||
val gaplessPlayback = Settings.gaplessPlayback
|
||||
|
||||
if (!gaplessPlayback) {
|
||||
if (!Settings.gaplessPlayback) {
|
||||
localMediaPlayer.clearNextPlaying(true)
|
||||
return
|
||||
}
|
||||
@ -218,9 +213,9 @@ 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.ALL -> index = (index + 1) % downloader.getPlaylist().size
|
||||
RepeatMode.SINGLE -> {
|
||||
}
|
||||
else -> {
|
||||
@ -229,8 +224,8 @@ class MediaPlayerService : Service() {
|
||||
}
|
||||
|
||||
localMediaPlayer.clearNextPlaying(false)
|
||||
if (index < downloader.playlist.size && index != -1) {
|
||||
localMediaPlayer.setNextPlaying(downloader.playlist[index])
|
||||
if (index < downloader.getPlaylist().size && index != -1) {
|
||||
localMediaPlayer.setNextPlaying(downloader.getPlaylist()[index])
|
||||
} else {
|
||||
localMediaPlayer.clearNextPlaying(true)
|
||||
}
|
||||
@ -283,16 +278,15 @@ class MediaPlayerService : Service() {
|
||||
@Synchronized
|
||||
fun play(index: Int, start: Boolean) {
|
||||
Timber.v("play requested for %d", index)
|
||||
if (index < 0 || index >= downloader.playlist.size) {
|
||||
if (index < 0 || index >= downloader.getPlaylist().size) {
|
||||
resetPlayback()
|
||||
} else {
|
||||
setCurrentPlaying(index)
|
||||
if (start) {
|
||||
if (jukeboxMediaPlayer.isEnabled) {
|
||||
jukeboxMediaPlayer.skip(index, 0)
|
||||
localMediaPlayer.setPlayerState(PlayerState.STARTED)
|
||||
} else {
|
||||
localMediaPlayer.play(downloader.playlist[index])
|
||||
localMediaPlayer.play(downloader.getPlaylist()[index])
|
||||
}
|
||||
}
|
||||
downloader.checkDownloads()
|
||||
@ -305,7 +299,7 @@ class MediaPlayerService : Service() {
|
||||
localMediaPlayer.reset()
|
||||
localMediaPlayer.setCurrentPlaying(null)
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex, playerPosition
|
||||
)
|
||||
}
|
||||
@ -318,7 +312,7 @@ class MediaPlayerService : Service() {
|
||||
} else {
|
||||
localMediaPlayer.pause()
|
||||
}
|
||||
localMediaPlayer.setPlayerState(PlayerState.PAUSED)
|
||||
localMediaPlayer.setPlayerState(PlayerState.PAUSED, localMediaPlayer.currentPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,7 +325,7 @@ class MediaPlayerService : Service() {
|
||||
localMediaPlayer.pause()
|
||||
}
|
||||
}
|
||||
localMediaPlayer.setPlayerState(PlayerState.STOPPED)
|
||||
localMediaPlayer.setPlayerState(PlayerState.STOPPED, null)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -341,7 +335,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?) {
|
||||
@ -354,100 +348,73 @@ class MediaPlayerService : Service() {
|
||||
UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false)
|
||||
}
|
||||
|
||||
private fun setupOnCurrentPlayingChangedHandler() {
|
||||
localMediaPlayer.onCurrentPlayingChanged = { currentPlaying: DownloadFile? ->
|
||||
private fun playerStateChangedHandler(
|
||||
playerState: PlayerState,
|
||||
currentPlaying: DownloadFile?
|
||||
) {
|
||||
val context = this@MediaPlayerService
|
||||
// AVRCP handles these separately so we must differentiate between the cases
|
||||
val isStateChanged = playerState != currentPlayerState
|
||||
val isTrackChanged = currentPlaying != currentTrack
|
||||
if (!isStateChanged && !isTrackChanged) return
|
||||
|
||||
if (currentPlaying != null) {
|
||||
Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song)
|
||||
Util.broadcastA2dpMetaDataChange(
|
||||
this@MediaPlayerService, playerPosition, currentPlaying,
|
||||
downloader.all.size, downloader.currentPlayingIndex + 1
|
||||
)
|
||||
} else {
|
||||
Util.broadcastNewTrackInfo(this@MediaPlayerService, null)
|
||||
Util.broadcastA2dpMetaDataChange(
|
||||
this@MediaPlayerService, playerPosition, null,
|
||||
downloader.all.size, downloader.currentPlayingIndex + 1
|
||||
)
|
||||
val showWhenPaused = playerState !== PlayerState.STOPPED &&
|
||||
Settings.isNotificationAlwaysEnabled
|
||||
|
||||
val show = playerState === PlayerState.STARTED || showWhenPaused
|
||||
val song = currentPlaying?.song
|
||||
|
||||
if (isStateChanged) {
|
||||
when {
|
||||
playerState === PlayerState.PAUSED -> {
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition
|
||||
)
|
||||
}
|
||||
playerState === PlayerState.STARTED -> {
|
||||
scrobbler.scrobble(currentPlaying, false)
|
||||
}
|
||||
playerState === PlayerState.COMPLETED -> {
|
||||
scrobbler.scrobble(currentPlaying, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Update widget
|
||||
val playerState = localMediaPlayer.playerState
|
||||
val song = currentPlaying?.song
|
||||
|
||||
updateWidget(playerState, song)
|
||||
|
||||
if (currentPlaying != null) {
|
||||
updateNotification(localMediaPlayer.playerState, currentPlaying)
|
||||
nowPlayingEventDistributor.raiseShowNowPlayingEvent()
|
||||
} else {
|
||||
nowPlayingEventDistributor.raiseHideNowPlayingEvent()
|
||||
stopForeground(true)
|
||||
isInForeground = false
|
||||
stopIfIdle()
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupOnPlayerStateChangedHandler() {
|
||||
localMediaPlayer.onPlayerStateChanged = {
|
||||
playerState: PlayerState,
|
||||
currentPlaying: DownloadFile?
|
||||
->
|
||||
|
||||
val context = this@MediaPlayerService
|
||||
|
||||
// Notify MediaSession
|
||||
mediaSessionHandler.updateMediaSession(
|
||||
currentPlaying,
|
||||
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
|
||||
downloader.getPlaylist().size,
|
||||
downloader.getPlaylist().indexOf(currentPlaying) + 1, playerPosition
|
||||
)
|
||||
} else {
|
||||
// State didn't change, only the track
|
||||
Util.broadcastA2dpMetaDataChange(
|
||||
this@MediaPlayerService, playerPosition, currentPlaying,
|
||||
downloader.all.size, downloader.currentPlayingIndex + 1
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if (isTrackChanged) {
|
||||
Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.song)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
currentPlayerState = playerState
|
||||
currentTrack = currentPlaying
|
||||
|
||||
Timber.d("Processed player state change")
|
||||
}
|
||||
|
||||
private fun setupOnSongCompletedHandler() {
|
||||
@ -465,9 +432,9 @@ class MediaPlayerService : Service() {
|
||||
}
|
||||
}
|
||||
if (index != -1) {
|
||||
when (repeatMode) {
|
||||
when (Settings.repeatMode) {
|
||||
RepeatMode.OFF -> {
|
||||
if (index + 1 < 0 || index + 1 >= downloader.playlist.size) {
|
||||
if (index + 1 < 0 || index + 1 >= downloader.getPlaylist().size) {
|
||||
if (Settings.shouldClearPlaylist) {
|
||||
clear(true)
|
||||
jukeboxMediaPlayer.updatePlaylist()
|
||||
@ -478,7 +445,7 @@ class MediaPlayerService : Service() {
|
||||
}
|
||||
}
|
||||
RepeatMode.ALL -> {
|
||||
play((index + 1) % downloader.playlist.size)
|
||||
play((index + 1) % downloader.getPlaylist().size)
|
||||
}
|
||||
RepeatMode.SINGLE -> play(index)
|
||||
else -> {
|
||||
@ -497,7 +464,7 @@ class MediaPlayerService : Service() {
|
||||
setNextPlaying()
|
||||
if (serialize) {
|
||||
playbackStateSerializer.serialize(
|
||||
downloader.playlist,
|
||||
downloader.getPlaylist(),
|
||||
downloader.currentPlayingIndex, playerPosition
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,86 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.view.KeyEvent
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
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
|
||||
|
||||
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())
|
||||
|
||||
val themeChangedEventPublisher: PublishSubject<Unit> =
|
||||
PublishSubject.create()
|
||||
val themeChangedEventObservable: Observable<Unit> =
|
||||
themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val playerStatePublisher: PublishSubject<StateWithTrack> =
|
||||
PublishSubject.create()
|
||||
val playerStateObservable: Observable<StateWithTrack> =
|
||||
playerStatePublisher.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)
|
||||
}
|
@ -26,6 +26,7 @@ import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.ShareDetails
|
||||
import org.moire.ultrasonic.util.TimeSpan
|
||||
import org.moire.ultrasonic.util.TimeSpanPicker
|
||||
import org.moire.ultrasonic.util.Util.ifNotNull
|
||||
|
||||
/**
|
||||
* This class handles sharing items in the media library
|
||||
@ -79,7 +80,7 @@ class ShareHandler(val context: Context) {
|
||||
|
||||
if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null
|
||||
if (shareDetails.Entries.isEmpty()) {
|
||||
fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)?.let {
|
||||
fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID).ifNotNull {
|
||||
ids.add(it)
|
||||
}
|
||||
} else {
|
||||
|
@ -1,68 +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)
|
||||
|
||||
synchronized(this) {
|
||||
if (cachedToken != null)
|
||||
listener.onMediaSessionTokenCreated(cachedToken!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun unsubscribe(listener: MediaSessionEventListener) {
|
||||
eventListenerList.remove(listener)
|
||||
}
|
||||
|
||||
fun releaseCachedMediaSessionToken() {
|
||||
synchronized(this) {
|
||||
cachedToken = null
|
||||
}
|
||||
}
|
||||
|
||||
fun raiseMediaSessionTokenCreatedEvent(token: MediaSessionCompat.Token) {
|
||||
synchronized(this) {
|
||||
cachedToken = token
|
||||
eventListenerList.forEach { listener -> listener.onMediaSessionTokenCreated(token) }
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
fun raiseMediaButtonEvent(keyEvent: KeyEvent?) {
|
||||
eventListenerList.forEach { listener -> listener.onMediaButtonEvent(keyEvent) }
|
||||
}
|
||||
}
|
@ -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,18 +17,21 @@ 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 org.moire.ultrasonic.util.Util.ifNotNull
|
||||
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
|
||||
*/
|
||||
@ -39,21 +42,22 @@ 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--
|
||||
if (referenceCount > 0) return
|
||||
|
||||
mediaSession?.isActive = false
|
||||
mediaSessionEventDistributor.releaseCachedMediaSessionToken()
|
||||
RxBus.releaseMediaSessionToken()
|
||||
rxBusSubscription.dispose()
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
|
||||
@ -72,7 +76,7 @@ class MediaSessionHandler : KoinComponent {
|
||||
|
||||
mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService")
|
||||
val mediaSessionToken = mediaSession?.sessionToken ?: return
|
||||
mediaSessionEventDistributor.raiseMediaSessionTokenCreatedEvent(mediaSessionToken)
|
||||
RxBus.mediaSessionTokenPublisher.onNext(mediaSessionToken)
|
||||
|
||||
updateMediaButtonReceiver()
|
||||
|
||||
@ -93,14 +97,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() {
|
||||
@ -147,28 +151,36 @@ class MediaSessionHandler : KoinComponent {
|
||||
// This probably won't be necessary once we implement more
|
||||
// of the modern media APIs, like the MediaController etc.
|
||||
val event = mediaButtonEvent.extras!!["android.intent.extra.KEY_EVENT"] as KeyEvent?
|
||||
mediaSessionEventDistributor.raiseMediaButtonEvent(event)
|
||||
event.ifNotNull { RxBus.mediaButtonEventPublisher.onNext(it) }
|
||||
return true
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@ -187,8 +199,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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,59 +256,46 @@ class MediaSessionHandler : KoinComponent {
|
||||
// Set actions
|
||||
playbackStateBuilder.setActions(playbackActions!!)
|
||||
|
||||
cachedPlayingIndex = currentPlayingIndex
|
||||
setMediaSessionQueue(cachedPlaylist)
|
||||
if (
|
||||
currentPlayingIndex != null && cachedPlaylist != null &&
|
||||
!Settings.shouldDisableNowPlayingListSending
|
||||
)
|
||||
playbackStateBuilder.setActiveQueueItemId(currentPlayingIndex)
|
||||
val index = cachedPlaylist?.indexOf(currentPlaying)
|
||||
cachedPlayingIndex = if (index == null || index < 0) null else index.toLong()
|
||||
cachedPlaylist.ifNotNull { setMediaSessionQueue(it) }
|
||||
|
||||
if (cachedPlaylist != null && !Settings.shouldDisableNowPlayingListSending)
|
||||
cachedPlayingIndex.ifNotNull { 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 (
|
||||
cachedPlayingIndex != null && cachedPlaylist != null &&
|
||||
!Settings.shouldDisableNowPlayingListSending
|
||||
)
|
||||
playbackStateBuilder.setActiveQueueItemId(cachedPlayingIndex!!)
|
||||
if (cachedPlaylist != null && !Settings.shouldDisableNowPlayingListSending)
|
||||
cachedPlayingIndex.ifNotNull { playbackStateBuilder.setActiveQueueItemId(it) }
|
||||
|
||||
mediaSession?.setPlaybackState(playbackStateBuilder.build())
|
||||
}
|
||||
|
@ -1,30 +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() }
|
||||
}
|
||||
|
||||
fun raiseNowPlayingDismissedEvent() {
|
||||
eventListenerList.forEach { listener -> listener.onDismissNowPlaying() }
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
/**
|
||||
* Callback interface for Now Playing event subscribers
|
||||
*/
|
||||
interface NowPlayingEventListener {
|
||||
fun onDismissNowPlaying()
|
||||
fun onHideNowPlaying()
|
||||
fun onShowNowPlaying()
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
/**
|
||||
* This class distributes Theme change related events to its subscribers.
|
||||
* It is a primitive implementation of a pub-sub event bus
|
||||
*/
|
||||
class ThemeChangedEventDistributor {
|
||||
var eventListenerList: MutableList<ThemeChangedEventListener> =
|
||||
listOf<ThemeChangedEventListener>().toMutableList()
|
||||
|
||||
fun subscribe(listener: ThemeChangedEventListener) {
|
||||
eventListenerList.add(listener)
|
||||
}
|
||||
|
||||
fun unsubscribe(listener: ThemeChangedEventListener) {
|
||||
eventListenerList.remove(listener)
|
||||
}
|
||||
|
||||
fun RaiseThemeChangedEvent() {
|
||||
eventListenerList.forEach { listener -> listener.onThemeChanged() }
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
/**
|
||||
* Callback interface for Theme change event subscribers
|
||||
*/
|
||||
interface ThemeChangedEventListener {
|
||||
fun onThemeChanged()
|
||||
}
|
@ -538,7 +538,6 @@ object Util {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Broadcasts the given song info as the new song being played.
|
||||
*/
|
||||
fun broadcastNewTrackInfo(context: Context, song: MusicDirectory.Entry?) {
|
||||
@ -957,6 +956,14 @@ object Util {
|
||||
return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given block if this is not null.
|
||||
* @return: the return of the block, or null if this is null
|
||||
*/
|
||||
fun <T : Any, R> T?.ifNotNull(block: (T) -> R): R? {
|
||||
return this?.let(block)
|
||||
}
|
||||
|
||||
data class NetworkInfo(
|
||||
var connected: Boolean = false,
|
||||
var unmetered: Boolean = false
|
||||
|
Loading…
x
Reference in New Issue
Block a user