diff --git a/dependencies.gradle b/dependencies.gradle
index 42ae817e..4aae7cf4 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -42,7 +42,9 @@ ext.versions = [
timber : "4.7.1",
fastScroll : "2.0.1",
colorPicker : "2.2.3",
- fsaf : "1.1"
+ fsaf : "1.1",
+ rxJava : "3.1.2",
+ rxAndroid : "3.0.0",
]
ext.gradlePlugins = [
@@ -91,6 +93,8 @@ ext.other = [
sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView",
colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker",
fsaf : "com.github.K1rakishou:Fuck-Storage-Access-Framework:$versions.fsaf",
+ rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava",
+ rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid",
]
ext.testing = [
diff --git a/detekt-baseline.xml b/detekt-baseline.xml
index 8adb941d..d983b21f 100644
--- a/detekt-baseline.xml
+++ b/detekt-baseline.xml
@@ -3,11 +3,12 @@
ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background
+ ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"
ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()
+ ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)
ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)
ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()
ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)
- FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent()
ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)
ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)
ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song)
@@ -49,7 +50,6 @@
NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()
NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )
NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()
- ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String
ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean
ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean
TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception
@@ -59,12 +59,10 @@
TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception
TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception
TooGenericExceptionCaught:SongView.kt$SongView$e: Exception
- TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable
TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song))
TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service
TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService
TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment
- UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler
UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle
diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle
index 017fa798..2895de49 100644
--- a/ultrasonic/build.gradle
+++ b/ultrasonic/build.gradle
@@ -107,6 +107,8 @@ dependencies {
implementation other.sortListView
implementation other.colorPickerView
implementation other.fsaf
+ implementation other.rxJava
+ implementation other.rxAndroid
kapt androidSupport.room
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java
deleted file mode 100644
index 4da43426..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java
+++ /dev/null
@@ -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 mediaPlayerControllerLazy = inject(MediaPlayerController.class);
- private final Lazy imageLoader = inject(ImageLoaderProvider.class);
- private final Lazy 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;
- }
-}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
index 3bd56c61..8ae51dee 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
@@ -20,7 +20,6 @@ package org.moire.ultrasonic.util;
import android.app.Activity;
import android.os.Handler;
-import org.moire.ultrasonic.service.CommunicationErrorHandler;
/**
* @author Sindre Mehus
@@ -54,12 +53,12 @@ public abstract class BackgroundTask implements ProgressListener
protected void error(Throwable error)
{
- CommunicationErrorHandler.Companion.handleError(error, activity);
+ CommunicationError.handleError(error, activity);
}
protected String getErrorMessage(Throwable error)
{
- return CommunicationErrorHandler.Companion.getErrorMessage(error, activity);
+ return CommunicationError.getErrorMessage(error, activity);
}
@Override
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/MyViewFlipper.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/MyViewFlipper.java
deleted file mode 100644
index 3d33e442..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/MyViewFlipper.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- This file is part of Subsonic.
-
- Subsonic is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Subsonic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Subsonic. If not, see .
-
- Copyright 2009 (C) Sindre Mehus
- */
-package org.moire.ultrasonic.util;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.ViewFlipper;
-
-/**
- * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191)
- *
- * @author Sindre Mehus
- * @version $Id$
- */
-public class MyViewFlipper extends ViewFlipper
-{
-
- public MyViewFlipper(Context context)
- {
- super(context);
- }
-
- public MyViewFlipper(Context context, AttributeSet attrs)
- {
- super(context, attrs);
- }
-
- @Override
- protected void onDetachedFromWindow()
- {
- try
- {
- super.onDetachedFromWindow();
- }
- catch (IllegalArgumentException e)
- {
- // Call stopFlipping() in order to kick off updateRunning()
- stopFlipping();
- }
- }
-}
-
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
index 1b5a92e9..55d52083 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
@@ -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,15 +44,12 @@ 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.NowPlayingEventDistributor
-import org.moire.ultrasonic.util.NowPlayingEventListener
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.UncaughtExceptionHandler
import org.moire.ultrasonic.util.Util
import timber.log.Timber
@@ -73,15 +71,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 activeServerProvider: ActiveServerProvider by inject()
private val serverRepository: ServerSettingDao by inject()
@@ -166,28 +162,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 ->
@@ -234,8 +224,8 @@ class NavigationActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
- nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
- themeChangedEventDistributor.unsubscribe(themeChangedEventListener)
+ themeChangedEventSubscription?.dispose()
+ playerStateSubscription?.dispose()
imageLoaderProvider.clearImageLoader()
}
@@ -382,8 +372,8 @@ class NavigationActivity : AppCompatActivity() {
private fun setUncaughtExceptionHandler() {
val handler = Thread.getDefaultUncaughtExceptionHandler()
- if (handler !is SubsonicUncaughtExceptionHandler) {
- Thread.setDefaultUncaughtExceptionHandler(SubsonicUncaughtExceptionHandler(this))
+ if (handler !is UncaughtExceptionHandler) {
+ Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler(this))
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt
index 044235b0..3d0298e5 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt
@@ -4,10 +4,7 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
-import org.moire.ultrasonic.util.MediaSessionEventDistributor
import org.moire.ultrasonic.util.MediaSessionHandler
-import org.moire.ultrasonic.util.NowPlayingEventDistributor
-import org.moire.ultrasonic.util.ThemeChangedEventDistributor
/**
* This Koin module contains the registration of general classes needed for Ultrasonic
@@ -15,8 +12,5 @@ import org.moire.ultrasonic.util.ThemeChangedEventDistributor
val applicationModule = module {
single { ActiveServerProvider(get()) }
single { ImageLoaderProvider(androidContext()) }
- single { NowPlayingEventDistributor() }
- single { ThemeChangedEventDistributor() }
- single { MediaSessionEventDistributor() }
single { MediaSessionHandler() }
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverter.kt
index 7699acf3..57f7e29d 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverter.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverter.kt
@@ -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
)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIShareConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIShareConverter.kt
index 36486783..408f42f7 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIShareConverter.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIShareConverter.kt
@@ -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.toDomainEntitiesList(): List = 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(),
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
index 54d26ef9..b9ca729a 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
@@ -220,6 +220,6 @@ class DownloadListModel(application: Application) : GenericListModel(application
private val downloader by inject()
fun getList(): LiveData> {
- return downloader.observableList
+ return downloader.observableDownloads
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt
index 693923ea..5ec1db0e 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt
@@ -18,9 +18,9 @@ import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.domain.MusicFolder
-import org.moire.ultrasonic.service.CommunicationErrorHandler
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory
+import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Settings
/**
@@ -94,7 +94,7 @@ open class GenericListModel(application: Application) :
private fun handleException(exception: Exception, context: Context) {
Handler(Looper.getMainLooper()).post {
- CommunicationErrorHandler.handleError(exception, context)
+ CommunicationError.handleError(exception, context)
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt
new file mode 100644
index 00000000..93b62077
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt
@@ -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
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt
index 31c61233..7556d148 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt
@@ -38,17 +38,22 @@ import androidx.fragment.app.Fragment
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
import java.util.Date
-import java.util.LinkedList
import java.util.Locale
+import java.util.concurrent.CancellationException
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.max
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
@@ -66,13 +71,14 @@ 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
import org.moire.ultrasonic.util.CancellationToken
+import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings
-import org.moire.ultrasonic.util.SilentBackgroundTask
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.AutoRepeatButton
import org.moire.ultrasonic.view.SongListAdapter
@@ -81,15 +87,13 @@ import timber.log.Timber
/**
* Contains the Music Player screen of Ultrasonic with playback controls and the playlist
- *
- * TODO: This class was more or less straight converted from Java legacy code.
- * There are many places where further cleanup would be nice.
- * The usage of threads and SilentBackgroundTask can be replaced with Coroutines.
*/
@Suppress("LargeClass", "TooManyFunctions", "MagicNumber")
-class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinComponent {
- // Settings
- private var currentRevision: Long = 0
+class PlayerFragment :
+ Fragment(),
+ GestureDetector.OnGestureListener,
+ KoinComponent,
+ CoroutineScope by CoroutineScope(Dispatchers.Main) {
private var swipeDistance = 0
private var swipeVelocity = 0
private var jukeboxAvailable = false
@@ -110,7 +114,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
private lateinit var executorService: ScheduledExecutorService
private var currentPlaying: DownloadFile? = null
private var currentSong: MusicDirectory.Entry? = null
- private var onProgressChangedTask: SilentBackgroundTask? = null
+ private var rxBusSubscription: Disposable? = null
+ private var ioScope = CoroutineScope(Dispatchers.IO)
// Views and UI Elements
private lateinit var visualizerViewLayout: LinearLayout
@@ -230,17 +235,11 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
previousButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
- object : SilentBackgroundTask(activity) {
- override fun doInBackground(): Void? {
- mediaPlayerController.previous()
- return null
- }
-
- override fun done(result: Void?) {
- onCurrentChanged()
- onSliderProgressChanged()
- }
- }.execute()
+ launch(CommunicationError.getHandler(context)) {
+ mediaPlayerController.previous()
+ onCurrentChanged()
+ onSliderProgressChanged()
+ }
}
previousButton.setOnRepeatListener {
@@ -250,65 +249,43 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
nextButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
- object : SilentBackgroundTask(activity) {
- override fun doInBackground(): Boolean {
- mediaPlayerController.next()
- return true
- }
-
- override fun done(result: Boolean?) {
- if (result == true) {
- onCurrentChanged()
- onSliderProgressChanged()
- }
- }
- }.execute()
+ launch(CommunicationError.getHandler(context)) {
+ mediaPlayerController.next()
+ onCurrentChanged()
+ onSliderProgressChanged()
+ }
}
nextButton.setOnRepeatListener {
val incrementTime = Settings.incrementTime
changeProgress(incrementTime)
}
+
pauseButton.setOnClickListener {
- object : SilentBackgroundTask(activity) {
- override fun doInBackground(): Void? {
- mediaPlayerController.pause()
- return null
- }
-
- override fun done(result: Void?) {
- onCurrentChanged()
- onSliderProgressChanged()
- }
- }.execute()
+ launch(CommunicationError.getHandler(context)) {
+ mediaPlayerController.pause()
+ onCurrentChanged()
+ onSliderProgressChanged()
+ }
}
+
stopButton.setOnClickListener {
- object : SilentBackgroundTask(activity) {
- override fun doInBackground(): Void? {
- mediaPlayerController.reset()
- return null
- }
-
- override fun done(result: Void?) {
- onCurrentChanged()
- onSliderProgressChanged()
- }
- }.execute()
+ launch(CommunicationError.getHandler(context)) {
+ mediaPlayerController.reset()
+ onCurrentChanged()
+ onSliderProgressChanged()
+ }
}
+
startButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
- object : SilentBackgroundTask(activity) {
- override fun doInBackground(): Void? {
- start()
- return null
- }
-
- override fun done(result: Void?) {
- onCurrentChanged()
- onSliderProgressChanged()
- }
- }.execute()
+ launch(CommunicationError.getHandler(context)) {
+ start()
+ onCurrentChanged()
+ onSliderProgressChanged()
+ }
}
+
shuffleButton.setOnClickListener {
mediaPlayerController.shuffle()
Util.toast(activity, R.string.download_menu_shuffle_notification)
@@ -335,16 +312,10 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onStopTrackingTouch(seekBar: SeekBar) {
- object : SilentBackgroundTask(activity) {
- override fun doInBackground(): Void? {
- mediaPlayerController.seekTo(progressBar.progress)
- return null
- }
-
- override fun done(result: Void?) {
- onSliderProgressChanged()
- }
- }.execute()
+ launch(CommunicationError.getHandler(context)) {
+ mediaPlayerController.seekTo(progressBar.progress)
+ onSliderProgressChanged()
+ }
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
@@ -353,18 +324,13 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
playlistView.setOnItemClickListener { _, _, position, _ ->
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
- object : SilentBackgroundTask(activity) {
- override fun doInBackground(): Void? {
- mediaPlayerController.play(position)
- return null
- }
-
- override fun done(result: Void?) {
- onCurrentChanged()
- onSliderProgressChanged()
- }
- }.execute()
+ launch(CommunicationError.getHandler(context)) {
+ mediaPlayerController.play(position)
+ onCurrentChanged()
+ onSliderProgressChanged()
+ }
}
+
registerForContextMenu(playlistView)
if (arguments != null && requireArguments().getBoolean(
@@ -419,13 +385,21 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
}
}
)
- Thread {
+
+ // Observe playlist changes and update the UI
+ rxBusSubscription = RxBus.playlistObservable.subscribe {
+ onPlaylistChanged()
+ }
+
+ // Query the Jukebox state in an IO Context
+ ioScope.launch(CommunicationError.getHandler(context)) {
try {
jukeboxAvailable = mediaPlayerController.isJukeboxAvailable
} catch (all: Exception) {
Timber.e(all)
}
- }.start()
+ }
+
view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) }
}
@@ -479,6 +453,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
}
override fun onDestroyView() {
+ rxBusSubscription?.dispose()
+ cancel("CoroutineScope cancelled because the view was destroyed")
cancellationToken.cancel()
super.onDestroyView()
}
@@ -797,9 +773,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()
}
@@ -810,33 +783,28 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
private fun savePlaylistInBackground(playlistName: String) {
Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName))
mediaPlayerController.suggestedPlaylistName = playlistName
- object : SilentBackgroundTask(activity) {
- @Throws(Throwable::class)
- override fun doInBackground(): Void? {
- val entries: MutableList = LinkedList()
- for (downloadFile in mediaPlayerController.playList) {
- entries.add(downloadFile.song)
- }
- val musicService = getMusicService()
- musicService.createPlaylist(null, playlistName, entries)
- return null
- }
- override fun done(result: Void?) {
+ ioScope.launch {
+
+ val entries = mediaPlayerController.playList.map {
+ it.song
+ }
+ val musicService = getMusicService()
+ musicService.createPlaylist(null, playlistName, entries)
+ }.invokeOnCompletion {
+ if (it == null || it is CancellationException) {
Util.toast(context, R.string.download_playlist_done)
- }
-
- override fun error(error: Throwable) {
- Timber.e(error, "Exception has occurred in savePlaylistInBackground")
+ } else {
+ Timber.e(it, "Exception has occurred in savePlaylistInBackground")
val msg = String.format(
Locale.ROOT,
"%s %s",
resources.getString(R.string.download_playlist_error),
- getErrorMessage(error)
+ CommunicationError.getErrorMessage(it, context)
)
Util.toast(context, msg)
}
- }.execute()
+ }
}
private fun toggleFullScreenAlbumArt() {
@@ -914,7 +882,6 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
emptyTextView.isVisible = list.isEmpty()
- currentRevision = mediaPlayerController.playListUpdateRevision
when (mediaPlayerController.repeatMode) {
RepeatMode.OFF -> repeatButton.setImageDrawable(
Util.getDrawableFromAttribute(
@@ -967,120 +934,95 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
}
}
+ @Suppress("LongMethod", "ComplexMethod")
+ @Synchronized
private fun onSliderProgressChanged() {
- if (onProgressChangedTask != null) {
- return
+
+ val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled
+ val millisPlayed: Int = max(0, mediaPlayerController.playerPosition)
+ val duration: Int = mediaPlayerController.playerDuration
+ val playerState: PlayerState = mediaPlayerController.playerState
+
+ if (cancellationToken.isCancellationRequested) return
+ if (currentPlaying != null) {
+ positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true)
+ durationTextView.text = Util.formatTotalDuration(duration.toLong(), true)
+ progressBar.max =
+ if (duration == 0) 100 else duration // Work-around for apparent bug.
+ progressBar.progress = millisPlayed
+ progressBar.isEnabled = currentPlaying!!.isWorkDone || isJukeboxEnabled
+ } else {
+ positionTextView.setText(R.string.util_zero_time)
+ durationTextView.setText(R.string.util_no_time)
+ progressBar.progress = 0
+ progressBar.max = 0
+ progressBar.isEnabled = false
}
- onProgressChangedTask = object : SilentBackgroundTask(activity) {
- var isJukeboxEnabled = false
- var millisPlayed = 0
- var duration: Int? = null
- var playerState: PlayerState? = null
- override fun doInBackground(): Void? {
- isJukeboxEnabled = mediaPlayerController.isJukeboxEnabled
- millisPlayed = max(0, mediaPlayerController.playerPosition)
- duration = mediaPlayerController.playerDuration
- playerState = mediaPlayerController.playerState
- return null
+
+ when (playerState) {
+ PlayerState.DOWNLOADING -> {
+ val progress =
+ if (currentPlaying != null) currentPlaying!!.progress.value!! else 0
+ val downloadStatus = resources.getString(
+ R.string.download_playerstate_downloading,
+ Util.formatPercentage(progress)
+ )
+ setTitle(this@PlayerFragment, downloadStatus)
}
-
- @Suppress("LongMethod")
- override fun done(result: Void?) {
- if (cancellationToken.isCancellationRequested) return
- if (currentPlaying != null) {
- val millisTotal = if (duration == null) 0 else duration!!
- positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true)
- durationTextView.text = Util.formatTotalDuration(millisTotal.toLong(), true)
- progressBar.max =
- if (millisTotal == 0) 100 else millisTotal // Work-around for apparent bug.
- progressBar.progress = millisPlayed
- progressBar.isEnabled = currentPlaying!!.isWorkDone || isJukeboxEnabled
- } else {
- positionTextView.setText(R.string.util_zero_time)
- durationTextView.setText(R.string.util_no_time)
- progressBar.progress = 0
- progressBar.max = 0
- progressBar.isEnabled = false
- }
-
- when (playerState) {
- PlayerState.DOWNLOADING -> {
- val progress =
- if (currentPlaying != null) currentPlaying!!.progress.value!! else 0
- val downloadStatus = resources.getString(
- R.string.download_playerstate_downloading,
- Util.formatPercentage(progress)
- )
- setTitle(this@PlayerFragment, downloadStatus)
- }
- PlayerState.PREPARING -> setTitle(
+ PlayerState.PREPARING -> setTitle(
+ this@PlayerFragment,
+ R.string.download_playerstate_buffering
+ )
+ PlayerState.STARTED -> {
+ if (mediaPlayerController.isShufflePlayEnabled) {
+ setTitle(
this@PlayerFragment,
- R.string.download_playerstate_buffering
+ R.string.download_playerstate_playing_shuffle
)
- PlayerState.STARTED -> {
- if (mediaPlayerController.isShufflePlayEnabled) {
- setTitle(
- this@PlayerFragment,
- R.string.download_playerstate_playing_shuffle
- )
- } else {
- setTitle(this@PlayerFragment, R.string.common_appname)
- }
- }
- PlayerState.IDLE,
- PlayerState.PREPARED,
- PlayerState.STOPPED,
- PlayerState.PAUSED,
- PlayerState.COMPLETED -> {
- }
- else -> setTitle(this@PlayerFragment, R.string.common_appname)
+ } else {
+ setTitle(this@PlayerFragment, R.string.common_appname)
}
+ }
+ PlayerState.IDLE,
+ PlayerState.PREPARED,
+ PlayerState.STOPPED,
+ PlayerState.PAUSED,
+ PlayerState.COMPLETED -> {
+ }
+ else -> setTitle(this@PlayerFragment, R.string.common_appname)
+ }
- when (playerState) {
- PlayerState.STARTED -> {
- pauseButton.isVisible = true
- stopButton.isVisible = false
- startButton.isVisible = false
- }
- PlayerState.DOWNLOADING, PlayerState.PREPARING -> {
- pauseButton.isVisible = false
- stopButton.isVisible = true
- startButton.isVisible = false
- }
- else -> {
- pauseButton.isVisible = false
- stopButton.isVisible = false
- startButton.isVisible = true
- }
- }
-
- // TODO: It would be a lot nicer if MediaPlayerController would send an event
- // when this is necessary instead of updating every time
- displaySongRating()
- onProgressChangedTask = null
+ when (playerState) {
+ PlayerState.STARTED -> {
+ pauseButton.isVisible = true
+ stopButton.isVisible = false
+ startButton.isVisible = false
+ }
+ PlayerState.DOWNLOADING, PlayerState.PREPARING -> {
+ pauseButton.isVisible = false
+ stopButton.isVisible = true
+ startButton.isVisible = false
+ }
+ else -> {
+ pauseButton.isVisible = false
+ stopButton.isVisible = false
+ startButton.isVisible = true
}
}
- onProgressChangedTask!!.execute()
+
+ // TODO: It would be a lot nicer if MediaPlayerController would send an event
+ // when this is necessary instead of updating every time
+ displaySongRating()
}
private fun changeProgress(ms: Int) {
- object : SilentBackgroundTask(activity) {
- var msPlayed = 0
- var duration: Int? = null
- var seekTo = 0
- override fun doInBackground(): Void? {
- msPlayed = max(0, mediaPlayerController.playerPosition)
- duration = mediaPlayerController.playerDuration
- val msTotal = duration!!
- seekTo = (msPlayed + ms).coerceAtMost(msTotal)
- mediaPlayerController.seekTo(seekTo)
- return null
- }
-
- override fun done(result: Void?) {
- progressBar.progress = seekTo
- }
- }.execute()
+ launch(CommunicationError.getHandler(context)) {
+ val msPlayed: Int = max(0, mediaPlayerController.playerPosition)
+ val duration = mediaPlayerController.playerDuration
+ val seekTo = (msPlayed + ms).coerceAtMost(duration)
+ mediaPlayerController.seekTo(seekTo)
+ progressBar.progress = seekTo
+ }
}
override fun onDown(me: MotionEvent): Boolean {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt
index 6a338301..a6d421cb 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt
@@ -37,6 +37,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.ultrasonicDirectory
@@ -45,7 +46,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
@@ -93,9 +93,6 @@ class SettingsFragment :
private val mediaPlayerControllerLazy = inject(
MediaPlayerController::class.java
)
- private val themeChangedEventDistributor = inject(
- ThemeChangedEventDistributor::class.java
- )
private val mediaSessionHandler = inject(
MediaSessionHandler::class.java
)
@@ -225,7 +222,7 @@ class SettingsFragment :
showArtistPicture!!.isEnabled = sharedPreferences.getBoolean(key, false)
}
Constants.PREFERENCES_KEY_THEME -> {
- themeChangedEventDistributor.value.RaiseThemeChangedEvent()
+ RxBus.themeChangedEventPublisher.onNext(Unit)
}
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
index c6681559..6394b08a 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
@@ -38,7 +38,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.getTitle
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
-import org.moire.ultrasonic.service.CommunicationErrorHandler
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
@@ -47,6 +46,7 @@ import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.CancellationToken
+import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings
@@ -211,7 +211,7 @@ class TrackCollectionFragment : Fragment() {
val handler = CoroutineExceptionHandler { _, exception ->
Handler(Looper.getMainLooper()).post {
- context?.let { CommunicationErrorHandler.handleError(exception, it) }
+ CommunicationError.handleError(exception, context)
}
refreshAlbumListView!!.isRefreshing = false
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt
index b8f55481..173c5806 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt
@@ -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()
private val lifecycleSupport by inject()
private val mediaSessionHandler by inject()
private val mediaPlayerController by inject()
@@ -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()
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt
deleted file mode 100644
index 1b876fb5..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- This file is part of Subsonic.
-
- Subsonic is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Subsonic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Subsonic. If not, see .
-
- Copyright 2020 (C) Jozsef Varga
- */
-package org.moire.ultrasonic.service
-
-import android.app.AlertDialog
-import android.content.Context
-import com.fasterxml.jackson.core.JsonParseException
-import java.io.FileNotFoundException
-import java.io.IOException
-import java.security.cert.CertPathValidatorException
-import java.security.cert.CertificateException
-import javax.net.ssl.SSLException
-import org.moire.ultrasonic.R
-import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
-import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
-import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
-import org.moire.ultrasonic.util.Util
-import timber.log.Timber
-
-/**
- * Contains helper functions to handle the exceptions
- * thrown during the communication with a Subsonic server
- */
-class CommunicationErrorHandler {
- companion object {
- fun handleError(error: Throwable?, context: Context) {
- Timber.w(error)
-
- AlertDialog.Builder(context)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setTitle(R.string.error_label)
- .setMessage(getErrorMessage(error!!, context))
- .setCancelable(true)
- .setPositiveButton(R.string.common_ok) { _, _ -> }
- .create().show()
- }
-
- fun getErrorMessage(error: Throwable, context: Context): String {
- if (error is IOException && !Util.isNetworkConnected()) {
- return context.resources.getString(R.string.background_task_no_network)
- } else if (error is FileNotFoundException) {
- return context.resources.getString(R.string.background_task_not_found)
- } else if (error is JsonParseException) {
- return context.resources.getString(R.string.background_task_parse_error)
- } else if (error is SSLException) {
- return if (
- error.cause is CertificateException &&
- error.cause?.cause is CertPathValidatorException
- ) {
- context.resources
- .getString(
- R.string.background_task_ssl_cert_error, error.cause?.cause?.message
- )
- } else {
- context.resources.getString(R.string.background_task_ssl_error)
- }
- } else if (error is ApiNotSupportedException) {
- return context.resources.getString(
- R.string.background_task_unsupported_api, error.serverApiVersion
- )
- } else if (error is IOException) {
- return context.resources.getString(R.string.background_task_network_error)
- } else if (error is SubsonicRESTException) {
- return error.getLocalizedErrorMessage(context)
- }
- val message = error.message
- return message ?: error.javaClass.simpleName
- }
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt
index 3f27c85d..a1aa4bec 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt
@@ -30,13 +30,17 @@ class Downloader(
private val externalStorageMonitor: ExternalStorageMonitor,
private val localMediaPlayer: LocalMediaPlayer
) : KoinComponent {
- val playlist: MutableList = ArrayList()
+
+ private val playlist = mutableListOf()
+
var started: Boolean = false
- private val downloadQueue: PriorityQueue = PriorityQueue()
- private val activelyDownloading: MutableList = ArrayList()
+ private val downloadQueue = PriorityQueue()
+ private val activelyDownloading = mutableListOf()
- val observableList: MutableLiveData> = MutableLiveData>()
+ // TODO: The playlist is now published with RX, while the observableDownloads is using LiveData.
+ // Use the same for both
+ val observableDownloads = MutableLiveData>()
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 = 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, 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
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
index 2dc5539b..f4ef95fb 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
@@ -32,12 +32,10 @@ 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
import timber.log.Timber
-import java.io.File
/**
* Represents a Media Player which uses the mobile's resources for playback
@@ -47,17 +45,10 @@ class LocalMediaPlayer : KoinComponent {
private val audioFocusHandler by inject()
private val context by inject()
- private val mediaSessionHandler by inject()
-
- @JvmField
- var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null
@JvmField
var onSongCompleted: ((DownloadFile?) -> Unit?)? = null
- @JvmField
- var onPlayerStateChanged: ((PlayerState, DownloadFile?) -> Unit?)? = null
-
@JvmField
var onPrepared: (() -> Any?)? = null
@@ -65,6 +56,7 @@ class LocalMediaPlayer : KoinComponent {
var onNextSongRequested: Runnable? = null
@JvmField
+ @Volatile
var playerState = PlayerState.IDLE
@JvmField
@@ -133,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)
@@ -165,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)
@@ -195,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))
}
/*
@@ -263,7 +246,7 @@ class LocalMediaPlayer : KoinComponent {
mediaPlayer = nextMediaPlayer!!
setCurrentPlaying(nextPlaying)
- setPlayerState(PlayerState.STARTED)
+ setPlayerState(PlayerState.STARTED, currentPlaying)
attachHandlersToPlayer(mediaPlayer, nextPlaying!!, false)
@@ -344,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()
@@ -355,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...
@@ -370,7 +354,6 @@ class LocalMediaPlayer : KoinComponent {
// downloadFile.updateModificationDate()
mediaPlayer.setOnCompletionListener(null)
- setPlayerState(PlayerState.IDLE)
setAudioAttributes(mediaPlayer)
var dataSource: String? = null
@@ -403,7 +386,7 @@ class LocalMediaPlayer : KoinComponent {
descriptor.close()
}
- setPlayerState(PlayerState.PREPARING)
+ setPlayerState(PlayerState.PREPARING, downloadFile)
mediaPlayer.setOnBufferingUpdateListener { mp, percent ->
val song = downloadFile.song
@@ -421,7 +404,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) {
@@ -436,9 +419,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)
}
}
@@ -446,6 +429,7 @@ class LocalMediaPlayer : KoinComponent {
onPrepared
}
}
+
attachHandlersToPlayer(mediaPlayer, downloadFile, partial)
mediaPlayer.prepareAsync()
} catch (x: Exception) {
@@ -541,7 +525,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
@@ -588,7 +572,7 @@ class LocalMediaPlayer : KoinComponent {
resetMediaPlayer()
try {
- setPlayerState(PlayerState.IDLE)
+ setPlayerState(PlayerState.IDLE, currentPlaying)
mediaPlayer.setOnErrorListener(null)
mediaPlayer.setOnCompletionListener(null)
} catch (x: Exception) {
@@ -617,7 +601,7 @@ class LocalMediaPlayer : KoinComponent {
private val partialFile: String = downloadFile.partialFile
override fun execute() {
- setPlayerState(PlayerState.DOWNLOADING)
+ setPlayerState(PlayerState.DOWNLOADING, downloadFile)
while (!bufferComplete() && !isOffline()) {
Util.sleepQuietly(1000L)
if (isCancelled) {
@@ -720,10 +704,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) {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
index c24e9e88..e3322213 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
@@ -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
@@ -417,6 +413,10 @@ class MediaPlayerController(
}
}
+ /**
+ * This function calls the music service directly and
+ * therefore can't be called from the main thread
+ */
val isJukeboxAvailable: Boolean
get() {
try {
@@ -479,6 +479,7 @@ class MediaPlayerController(
Timber.e(e)
}
}.start()
+ // TODO this would be better handled with a Rx command
updateNotification()
}
@@ -490,16 +491,13 @@ class MediaPlayerController(
}
val playlistSize: Int
- get() = downloader.playlist.size
+ get() = downloader.getPlaylist().size
val currentPlayingNumberOnPlaylist: Int
get() = downloader.currentPlayingIndex
val playList: List
- get() = downloader.playlist
-
- val playListUpdateRevision: Long
- get() = downloader.playlistUpdateRevision
+ get() = downloader.getPlaylist()
val playListDuration: Long
get() = downloader.downloadListDuration
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt
index c69b820b..379b6d60 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt
@@ -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()
private val mediaPlayerController by inject()
private val downloader by inject()
- private val mediaSessionEventDistributor by inject()
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)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
index 21a8e5f8..e503b36f 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
@@ -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()
private val downloader by inject()
private val localMediaPlayer by inject()
- private val nowPlayingEventDistributor by inject()
- private val mediaSessionEventDistributor by inject()
private val mediaSessionHandler by inject()
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
)
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt
index 4e3c0692..df9e4390 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt
@@ -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()
- private val mediaSessionHandler by inject()
- 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)
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt
new file mode 100644
index 00000000..eeca3ffc
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt
@@ -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 =
+ PublishSubject.create()
+ val mediaSessionTokenObservable: Observable =
+ mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread())
+ .replay(1)
+ .autoConnect(0)
+
+ val mediaButtonEventPublisher: PublishSubject =
+ PublishSubject.create()
+ val mediaButtonEventObservable: Observable =
+ mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread())
+
+ val themeChangedEventPublisher: PublishSubject =
+ PublishSubject.create()
+ val themeChangedEventObservable: Observable =
+ themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread())
+
+ val playerStatePublisher: PublishSubject =
+ PublishSubject.create()
+ val playerStateObservable: Observable =
+ playerStatePublisher.observeOn(AndroidSchedulers.mainThread())
+ .replay(1)
+ .autoConnect(0)
+
+ val playlistPublisher: PublishSubject> =
+ PublishSubject.create()
+ val playlistObservable: Observable> =
+ playlistPublisher.observeOn(AndroidSchedulers.mainThread())
+ .replay(1)
+ .autoConnect(0)
+
+ val playbackPositionPublisher: PublishSubject =
+ PublishSubject.create()
+ val playbackPositionObservable: Observable =
+ playbackPositionPublisher.observeOn(AndroidSchedulers.mainThread())
+ .throttleFirst(1, TimeUnit.SECONDS)
+ .replay(1)
+ .autoConnect(0)
+
+ // Commands
+ val dismissNowPlayingCommandPublisher: PublishSubject =
+ PublishSubject.create()
+ val dismissNowPlayingCommandObservable: Observable =
+ dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread())
+
+ val playFromMediaIdCommandPublisher: PublishSubject> =
+ PublishSubject.create()
+ val playFromMediaIdCommandObservable: Observable> =
+ playFromMediaIdCommandPublisher.observeOn(AndroidSchedulers.mainThread())
+
+ val playFromSearchCommandPublisher: PublishSubject> =
+ PublishSubject.create()
+ val playFromSearchCommandObservable: Observable> =
+ playFromSearchCommandPublisher.observeOn(AndroidSchedulers.mainThread())
+
+ val skipToQueueItemCommandPublisher: PublishSubject =
+ PublishSubject.create()
+ val skipToQueueItemCommandObservable: Observable =
+ 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)
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt
index 883aeeac..13f9922f 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt
@@ -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 {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt
index 4f6f0598..1d6e9e33 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt
@@ -1,10 +1,13 @@
package org.moire.ultrasonic.util
-import android.os.AsyncTask
import android.os.StatFs
import android.system.Os
import java.util.ArrayList
import java.util.HashSet
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Playlist
@@ -22,105 +25,85 @@ import timber.log.Timber
/**
* Responsible for cleaning up files from the offline download cache on the filesystem.
*/
-class CacheCleaner {
+class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
+
+ private fun exceptionHandler(tag: String): CoroutineExceptionHandler {
+ return CoroutineExceptionHandler { _, exception ->
+ Timber.w(exception, "Exception in CacheCleaner.$tag")
+ }
+ }
+
fun clean() {
- try {
- BackgroundCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
- } catch (all: Exception) {
- // If an exception is thrown, assume we execute correctly the next time
- Timber.w(all, "Exception in CacheCleaner.clean")
+ launch(exceptionHandler("clean")) {
+ backgroundCleanup()
}
}
fun cleanSpace() {
- try {
- BackgroundSpaceCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
- } catch (all: Exception) {
- // If an exception is thrown, assume we execute correctly the next time
- Timber.w(all, "Exception in CacheCleaner.cleanSpace")
+ launch(exceptionHandler("cleanSpace")) {
+ backgroundSpaceCleanup()
}
}
fun cleanPlaylists(playlists: List) {
- try {
- BackgroundPlaylistsCleanup().executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR,
- playlists
- )
- } catch (all: Exception) {
- // If an exception is thrown, assume we execute correctly the next time
- Timber.w(all, "Exception in CacheCleaner.cleanPlaylists")
+ launch(exceptionHandler("cleanPlaylists")) {
+ backgroundPlaylistsCleanup(playlists)
}
}
- private class BackgroundCleanup : AsyncTask() {
- override fun doInBackground(vararg params: Void?): Void? {
- try {
- Thread.currentThread().name = "BackgroundCleanup"
- val files: MutableList = ArrayList()
- val dirs: MutableList = ArrayList()
+ private fun backgroundCleanup() {
+ try {
+ val files: MutableList = ArrayList()
+ val dirs: MutableList = ArrayList()
- findCandidatesForDeletion(musicDirectory, files, dirs)
+ findCandidatesForDeletion(musicDirectory, files, dirs)
+ sortByAscendingModificationTime(files)
+ val filesToNotDelete = findFilesToNotDelete()
+ deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true)
+ deleteEmptyDirs(dirs, filesToNotDelete)
+ } catch (all: RuntimeException) {
+ Timber.e(all, "Error in cache cleaning.")
+ }
+ }
+
+ private fun backgroundSpaceCleanup() {
+ try {
+ val files: MutableList = ArrayList()
+ val dirs: MutableList = ArrayList()
+
+ findCandidatesForDeletion(musicDirectory, files, dirs)
+
+ val bytesToDelete = getMinimumDelete(files)
+ if (bytesToDelete > 0L) {
sortByAscendingModificationTime(files)
val filesToNotDelete = findFilesToNotDelete()
-
- deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true)
- deleteEmptyDirs(dirs, filesToNotDelete)
- } catch (all: RuntimeException) {
- Timber.e(all, "Error in cache cleaning.")
+ deleteFiles(files, filesToNotDelete, bytesToDelete, false)
}
- return null
+ } catch (all: RuntimeException) {
+ Timber.e(all, "Error in cache cleaning.")
}
}
- private class BackgroundSpaceCleanup : AsyncTask() {
- override fun doInBackground(vararg params: Void?): Void? {
- try {
- Thread.currentThread().name = "BackgroundSpaceCleanup"
+ private fun backgroundPlaylistsCleanup(vararg params: List) {
+ try {
+ val activeServerProvider = inject(
+ ActiveServerProvider::class.java
+ )
- val files: MutableList = ArrayList()
- val dirs: MutableList = ArrayList()
+ val server = activeServerProvider.value.getActiveServer().name
+ val playlistFiles = listFiles(getPlaylistDirectory(server))
+ val playlists = params[0]
- findCandidatesForDeletion(musicDirectory, files, dirs)
-
- val bytesToDelete = getMinimumDelete(files)
-
- if (bytesToDelete > 0L) {
- sortByAscendingModificationTime(files)
- val filesToNotDelete = findFilesToNotDelete()
- deleteFiles(files, filesToNotDelete, bytesToDelete, false)
- }
- } catch (all: RuntimeException) {
- Timber.e(all, "Error in cache cleaning.")
+ for ((_, name) in playlists) {
+ playlistFiles.remove(getPlaylistFile(server, name))
}
- return null
- }
- }
- private class BackgroundPlaylistsCleanup : AsyncTask, Void?, Void?>() {
- override fun doInBackground(vararg params: List): Void? {
- try {
- val activeServerProvider = inject(
- ActiveServerProvider::class.java
- )
- Thread.currentThread().name = "BackgroundPlaylistsCleanup"
-
- val server = activeServerProvider.value.getActiveServer().name
- val playlistFiles = listFiles(getPlaylistDirectory(server))
- val playlists = params[0]
-
- for ((_, name) in playlists) {
- playlistFiles.remove(getPlaylistFile(server, name))
- }
-
- for (playlist in playlistFiles) {
- playlist.delete()
- }
- } catch (all: RuntimeException) {
- Timber.e(all, "Error in playlist cache cleaning.")
+ for (playlist in playlistFiles) {
+ playlist.delete()
}
- return null
+ } catch (all: RuntimeException) {
+ Timber.e(all, "Error in playlist cache cleaning.")
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt
new file mode 100644
index 00000000..2a722e26
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt
@@ -0,0 +1,91 @@
+/*
+ * CommunicationErrorUtil.kt
+ * Copyright (C) 2009-2021 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+package org.moire.ultrasonic.util
+
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import com.fasterxml.jackson.core.JsonParseException
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.security.cert.CertPathValidatorException
+import java.security.cert.CertificateException
+import javax.net.ssl.SSLException
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineExceptionHandler
+import org.moire.ultrasonic.R
+import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
+import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
+import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
+import timber.log.Timber
+
+/**
+ * Contains helper functions to handle the exceptions
+ * thrown during the communication with a Subsonic server
+ */
+object CommunicationError {
+ fun getHandler(context: Context?, handler: ((CoroutineContext, Throwable) -> Unit)? = null):
+ CoroutineExceptionHandler {
+ return CoroutineExceptionHandler { coroutineContext, exception ->
+ Handler(Looper.getMainLooper()).post {
+ handleError(exception, context)
+ handler?.invoke(coroutineContext, exception)
+ }
+ }
+ }
+
+ @JvmStatic
+ 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)
+ .setMessage(getErrorMessage(error!!, context))
+ .setCancelable(true)
+ .setPositiveButton(R.string.common_ok) { _, _ -> }
+ .create().show()
+ }
+
+ @JvmStatic
+ @Suppress("ReturnCount")
+ fun getErrorMessage(error: Throwable, context: Context?): String {
+ if (context == null) return "Couldn't get Error message, Context is null"
+ if (error is IOException && !Util.isNetworkConnected()) {
+ return context.resources.getString(R.string.background_task_no_network)
+ } else if (error is FileNotFoundException) {
+ return context.resources.getString(R.string.background_task_not_found)
+ } else if (error is JsonParseException) {
+ return context.resources.getString(R.string.background_task_parse_error)
+ } else if (error is SSLException) {
+ return if (
+ error.cause is CertificateException &&
+ error.cause?.cause is CertPathValidatorException
+ ) {
+ context.resources
+ .getString(
+ R.string.background_task_ssl_cert_error, error.cause?.cause?.message
+ )
+ } else {
+ context.resources.getString(R.string.background_task_ssl_error)
+ }
+ } else if (error is ApiNotSupportedException) {
+ return context.resources.getString(
+ R.string.background_task_unsupported_api, error.serverApiVersion
+ )
+ } else if (error is IOException) {
+ return context.resources.getString(R.string.background_task_network_error)
+ } else if (error is SubsonicRESTException) {
+ return error.getLocalizedErrorMessage(context)
+ }
+ val message = error.message
+ return message ?: error.javaClass.simpleName
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt
deleted file mode 100644
index a9ecade8..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventDistributor.kt
+++ /dev/null
@@ -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 =
- listOf().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) }
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt
deleted file mode 100644
index e4075248..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionEventListener.kt
+++ /dev/null
@@ -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?) {}
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt
index d7ebf424..f7bdf220 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt
@@ -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()
private val applicationContext by inject()
private var referenceCount: Int = 0
- private var cachedPlaylist: List? = null
- private var playbackPositionDelayCount: Int = 0
+ private var cachedPlaylist: List? = 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) {
- // 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) {
+ cachedPlaylist = playlist
+ setMediaSessionQueue(playlist)
}
- private fun setMediaSessionQueue(queue: List?) {
+ private fun setMediaSessionQueue(playlist: List) {
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())
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt
deleted file mode 100644
index 0d19903e..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventDistributor.kt
+++ /dev/null
@@ -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 =
- listOf().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() }
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt
deleted file mode 100644
index 3f4bd75e..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/NowPlayingEventListener.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.moire.ultrasonic.util
-
-/**
- * Callback interface for Now Playing event subscribers
- */
-interface NowPlayingEventListener {
- fun onDismissNowPlaying()
- fun onHideNowPlaying()
- fun onShowNowPlaying()
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt
deleted file mode 100644
index 3639aa2c..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * SilentBackgroundTask.kt
- * Copyright (C) 2009-2021 Ultrasonic developers
- *
- * Distributed under terms of the GNU GPLv3 license.
- */
-
-package org.moire.ultrasonic.util
-
-import android.app.Activity
-
-/**
- * @author Sindre Mehus
- */
-abstract class SilentBackgroundTask(activity: Activity?) : BackgroundTask(activity) {
- override fun execute() {
- val thread: Thread = object : Thread() {
- override fun run() {
- try {
- val result = doInBackground()
- handler.post { done(result) }
- } catch (all: Throwable) {
- handler.post { error(all) }
- }
- }
- }
- thread.start()
- }
-
- override fun updateProgress(messageId: Int) {}
- override fun updateProgress(message: String) {}
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt
index 74d292d5..2ba10bb2 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt
@@ -37,6 +37,10 @@ class StorageFile private constructor(
return getPath().compareTo(other.getPath())
}
+ override fun toString(): String {
+ return name
+ }
+
var name: String = fileManager.getName(abstractFile)
var isDirectory: Boolean = fileManager.isDirectory(abstractFile)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventDistributor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventDistributor.kt
deleted file mode 100644
index bdce05ab..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventDistributor.kt
+++ /dev/null
@@ -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 =
- listOf().toMutableList()
-
- fun subscribe(listener: ThemeChangedEventListener) {
- eventListenerList.add(listener)
- }
-
- fun unsubscribe(listener: ThemeChangedEventListener) {
- eventListenerList.remove(listener)
- }
-
- fun RaiseThemeChangedEvent() {
- eventListenerList.forEach { listener -> listener.onThemeChanged() }
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventListener.kt
deleted file mode 100644
index 5656f1d4..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ThemeChangedEventListener.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.moire.ultrasonic.util
-
-/**
- * Callback interface for Theme change event subscribers
- */
-interface ThemeChangedEventListener {
- fun onThemeChanged()
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/UncaughtExceptionHandler.kt
similarity index 91%
rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt
rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/UncaughtExceptionHandler.kt
index 4e6d1834..dd06f788 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/UncaughtExceptionHandler.kt
@@ -10,7 +10,7 @@ import java.io.File
/**
* Logs the stack trace of uncaught exceptions to a file on the SD card.
*/
-class SubsonicUncaughtExceptionHandler(
+class UncaughtExceptionHandler(
private val context: Context
) : Thread.UncaughtExceptionHandler {
private val defaultHandler: Thread.UncaughtExceptionHandler? =
@@ -32,8 +32,8 @@ class SubsonicUncaughtExceptionHandler(
throwable.printStackTrace(printWriter)
Timber.e(throwable, "Uncaught Exception! %s", logMessage)
Timber.i("Stack trace written to %s", file)
- } catch (x: Throwable) {
- Timber.e(x, "Failed to write stack trace to %s", file)
+ } catch (all: Throwable) {
+ Timber.e(all, "Failed to write stack trace to %s", file)
} finally {
printWriter.safeClose()
defaultHandler?.uncaughtException(thread, throwable)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
index 52f51bd5..d0604dcc 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
@@ -43,8 +43,6 @@ import android.widget.Toast
import androidx.annotation.AnyRes
import androidx.media.utils.MediaConstants
import java.io.Closeable
-import java.io.IOException
-import java.io.File
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.text.DecimalFormat
@@ -482,7 +480,6 @@ object Util {
}
/**
- *
* Broadcasts the given song info as the new song being played.
*/
fun broadcastNewTrackInfo(context: Context, song: MusicDirectory.Entry?) {
@@ -902,6 +899,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?.ifNotNull(block: (T) -> R): R? {
+ return this?.let(block)
+ }
+
/**
* Small data class to store information about the current network
**/
diff --git a/ultrasonic/src/main/res/layout-land/current_playing.xml b/ultrasonic/src/main/res/layout-land/current_playing.xml
index a3028829..3d7b1acb 100644
--- a/ultrasonic/src/main/res/layout-land/current_playing.xml
+++ b/ultrasonic/src/main/res/layout-land/current_playing.xml
@@ -1,21 +1,23 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ a:layout_width="fill_parent"
+ a:layout_height="fill_parent"
+ a:baselineAligned="false"
+ a:orientation="horizontal">
-
+ a:layout_weight="1"
+ tools:ignore="UselessParent">
+ a:orientation="vertical">
-
+
@@ -60,10 +63,11 @@
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
- a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
+ a:importantForAccessibility="no"
+ a:padding="10dip"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
@@ -72,10 +76,11 @@
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
- a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
+ a:importantForAccessibility="no"
+ a:padding="10dip"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
@@ -84,10 +89,11 @@
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
- a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
+ a:importantForAccessibility="no"
+ a:padding="10dip"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
@@ -96,10 +102,11 @@
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
- a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
+ a:importantForAccessibility="no"
+ a:padding="10dip"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
@@ -113,15 +120,16 @@
a:layout_marginStart="60dip"
a:layout_marginEnd="60dip"
a:background="@color/translucent"
- a:orientation="vertical"/>
+ a:orientation="vertical" />
-
-
+
+
+
-
-
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/current_playing.xml b/ultrasonic/src/main/res/layout/current_playing.xml
index 806ceb04..b4a7d754 100644
--- a/ultrasonic/src/main/res/layout/current_playing.xml
+++ b/ultrasonic/src/main/res/layout/current_playing.xml
@@ -4,7 +4,7 @@
a:layout_height="fill_parent"
a:orientation="vertical" >
-
@@ -52,7 +51,8 @@
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
- a:src="?attr/star_hollow" />
+ a:src="?attr/star_hollow"
+ a:importantForAccessibility="no" />
+ a:src="?attr/star_hollow"
+ a:importantForAccessibility="no" />
+ a:src="?attr/star_hollow"
+ a:importantForAccessibility="no" />
+ a:src="?attr/star_hollow"
+ a:importantForAccessibility="no" />
+ a:src="?attr/star_hollow"
+ a:importantForAccessibility="no" />
@@ -119,7 +123,7 @@
-
+
diff --git a/ultrasonic/src/main/res/layout/current_playlist.xml b/ultrasonic/src/main/res/layout/current_playlist.xml
index 1b933376..fcee1849 100644
--- a/ultrasonic/src/main/res/layout/current_playlist.xml
+++ b/ultrasonic/src/main/res/layout/current_playlist.xml
@@ -5,8 +5,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical"
a:layout_width="fill_parent"
- a:layout_height="fill_parent"
- a:layout_weight="1">
+ a:layout_height="fill_parent">
+ a:textAppearance="?android:attr/textAppearanceSmall"
+ tools:ignore="HardcodedText" />