From c294c446a38ed051ddcc0bb9ef35c3e9beda6b83 Mon Sep 17 00:00:00 2001 From: Nite Date: Sat, 24 Sep 2022 20:43:18 +0000 Subject: [PATCH] Fixed server change --- core/subsonic-api/build.gradle | 1 - .../ultrasonic/fragment/ChatFragment.java | 37 +++---------- .../ultrasonic/activity/NavigationActivity.kt | 2 +- .../ultrasonic/data/ActiveServerProvider.kt | 20 +++++-- .../fragment/ServerSelectorFragment.kt | 27 +-------- .../ultrasonic/playback/PlaybackService.kt | 10 +--- .../ultrasonic/service/DownloadService.kt | 3 +- .../ultrasonic/service/JukeboxMediaPlayer.kt | 55 ++++++++++++------- .../service/JukeboxUnimplementedFunctions.kt | 8 --- .../service/MediaPlayerController.kt | 35 +++++++++--- .../org/moire/ultrasonic/service/RxBus.kt | 13 ++++- .../kotlin/org/moire/ultrasonic/util/Util.kt | 14 ++++- 12 files changed, 117 insertions(+), 108 deletions(-) diff --git a/core/subsonic-api/build.gradle b/core/subsonic-api/build.gradle index 6db3ffcb..a70a11a2 100644 --- a/core/subsonic-api/build.gradle +++ b/core/subsonic-api/build.gradle @@ -10,7 +10,6 @@ dependencies { } implementation libs.kotlinReflect // for jackson kotlin, but to use the same version implementation libs.okhttpLogging - implementation libs.timber testImplementation libs.kotlinJunit testImplementation libs.mockito diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java index f38704ee..bdf37539 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java @@ -83,15 +83,7 @@ public class ChatFragment extends Fragment { messageEditText = view.findViewById(R.id.chat_edittext); sendButton = view.findViewById(R.id.chat_send); - sendButton.setOnClickListener(new View.OnClickListener() - { - - @Override - public void onClick(View view) - { - sendMessage(); - } - }); + sendButton.setOnClickListener(view1 -> sendMessage()); chatListView = view.findViewById(R.id.chat_entries_list); chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); @@ -125,20 +117,14 @@ public class ChatFragment extends Fragment { } }); - messageEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() - { - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) + messageEditText.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN)) { - if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN)) - { - sendMessage(); - return true; - } - - return false; + sendMessage(); + return true; } + + return false; }); load(); @@ -215,14 +201,7 @@ public class ChatFragment extends Fragment { @Override public void run() { - getActivity().runOnUiThread(new Runnable() - { - @Override - public void run() - { - load(); - } - }); + getActivity().runOnUiThread(() -> load()); } }, refreshInterval, refreshInterval); } 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 ae29ad70..23b00828 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -211,7 +211,7 @@ class NavigationActivity : AppCompatActivity() { recreate() } - rxBusSubscription += RxBus.activeServerChangeObservable.subscribe { + rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { updateNavigationHeaderForServer() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index 991d6698..34903985 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -7,6 +7,8 @@ package org.moire.ultrasonic.data +import android.os.Handler +import android.os.Looper import androidx.room.Room import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -234,12 +236,22 @@ class ActiveServerProvider( * @param serverId: The id of the desired server */ fun setActiveServerById(serverId: Int) { - resetMusicService() + val oldServerId = Settings.activeServer + if (oldServerId == serverId) return - Settings.activeServer = serverId + // Notify components about the change before actually resetting the MusicService + // so they can react by e.g. stopping playback on the old server + RxBus.activeServerChangingPublisher.onNext(oldServerId) - Timber.i("setActiveServerById done, new id: %s", serverId) - RxBus.activeServerChangePublisher.onNext(serverId) + // Post the server change to the end of the message queue, + // so the cleanup have time to finish + Handler(Looper.getMainLooper()).post { + resetMusicService() + Settings.activeServer = serverId + + RxBus.activeServerChangedPublisher.onNext(serverId) + Timber.i("setActiveServerById done, new id: %s", serverId) + } } /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index e364ec0f..9db2cb6b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -17,8 +17,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.model.ServerSettingsModel -import org.moire.ultrasonic.service.DownloadService -import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -30,7 +28,6 @@ class ServerSelectorFragment : Fragment() { private var listView: ListView? = null private val serverSettingsModel: ServerSettingsModel by viewModel() - private val controller: MediaPlayerController by inject() private val activeServerProvider: ActiveServerProvider by inject() private var serverRowAdapter: ServerRowAdapter? = null @@ -68,7 +65,7 @@ class ServerSelectorFragment : Fragment() { listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ -> val server = parent.getItemAtPosition(position) as ServerSetting - setActiveServerById(server.id) + ActiveServerProvider.setActiveServerById(server.id) findNavController().popBackStack(R.id.mainFragment, false) } @@ -88,26 +85,6 @@ class ServerSelectorFragment : Fragment() { } } - /** - * Sets the active server when a list item is clicked - */ - private fun setActiveServerById(id: Int) { - val oldId = activeServerProvider.getActiveServer().id - - // Check if there is a change - if (oldId == id) - return - - // Remove incomplete tracks if we are going offline, or changing between servers. - // If we are coming from offline there is no need to clear downloads etc. - if (oldId != OFFLINE_DB_ID) { - controller.removeIncompleteTracksFromPlaylist() - DownloadService.requestStop() - } - - ActiveServerProvider.setActiveServerById(id) - } - /** * This Callback handles the deletion of a Server Setting */ @@ -122,7 +99,7 @@ class ServerSelectorFragment : Fragment() { val activeServerId = ActiveServerProvider.getActiveServerId() // If the currently active server is deleted, go offline - if (id == activeServerId) setActiveServerById(OFFLINE_DB_ID) + if (id == activeServerId) ActiveServerProvider.setActiveServerById(OFFLINE_DB_ID) serverSettingsModel.deleteItemById(id) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index aaebe797..7f3c8145 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -37,6 +37,7 @@ import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification import org.moire.ultrasonic.util.toTrack import timber.log.Timber @@ -86,12 +87,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { mediaLibrarySession.release() rxBusSubscription.dispose() isStarted = false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - stopForeground(STOP_FOREGROUND_REMOVE) - } else { - @Suppress("DEPRECATION") - stopForeground(true) - } + stopForegroundRemoveNotification() stopSelf() } @@ -155,7 +151,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { .build() // Set a listener to update the API client when the active server has changed - rxBusSubscription += RxBus.activeServerChangeObservable.subscribe { + rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { // Set the player wake mode player.setWakeMode(getWakeModeFlag()) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index eb75a7f5..d473bc54 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -38,6 +38,7 @@ import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.SimpleServiceBinder import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification import timber.log.Timber private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic" @@ -94,7 +95,7 @@ class DownloadService : Service(), KoinComponent { isShuttingDown = true isInForeground = false - stopForeground(true) + stopForegroundRemoveNotification() wifiLock?.release() wifiLock = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index 20024844..f2442a44 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -60,11 +60,13 @@ import org.moire.ultrasonic.playback.MediaNotificationProvider import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.Util.getPendingIntentToShowPlayer import org.moire.ultrasonic.util.Util.sleepQuietly +import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification import timber.log.Timber private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L private const val SEEK_INCREMENT_SECONDS = 5L private const val SEEK_START_AFTER_SECONDS = 5 +private const val QUEUE_POLL_INTERVAL_SECONDS = 1L /** * Provides an asynchronous interface to the remote jukebox on the Subsonic server. @@ -159,17 +161,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } override fun onDestroy() { + tasks.clear() + stop() + if (!running.get()) return running.set(false) - sleepQuietly(1000) - if (serviceThread != null) { - serviceThread!!.interrupt() - } + serviceThread!!.join() - tasks.clear() - stop() - stopForeground(true) + stopForegroundRemoveNotification() mediaSession.release() super.onDestroy() @@ -417,9 +417,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { if (playlist.isEmpty()) return 0 if (currentIndex < 0 || currentIndex >= playlist.size) return 0 - return ( - playlist[currentIndex].mediaMetadata.extras?.getInt("duration") ?: 0 - ).toLong() * 1000 + return (playlist[currentIndex].mediaMetadata.extras?.getInt("duration") ?: 0) + .toLong() * 1000 } override fun getContentDuration(): Long { @@ -449,6 +448,17 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { seekTo(currentIndex, 0) } + override fun setMediaItems( + mediaItems: MutableList, + startIndex: Int, + startPositionMs: Long + ) { + playlist.clear() + playlist.addAll(mediaItems) + updatePlaylist() + seekTo(startIndex, startPositionMs) + } + private fun startProcessTasks() { serviceThread = object : Thread() { override fun run() { @@ -481,22 +491,28 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } } + @Suppress("LoopWithTooManyJumpStatements") private fun processTasks() { - while (running.get()) { + Timber.d("JukeboxMediaPlayer processTasks starting") + while (true) { // Sleep a bit to spare processor time if we loop a lot sleepQuietly(10) + // This is only necessary if Ultrasonic goes offline sooner than the thread stops + if (isOffline()) continue var task: JukeboxTask? = null try { - if (!isOffline()) { - task = tasks.take() - val status = task.execute() - onStatusUpdate(status) - } - } catch (ignored: InterruptedException) { + task = tasks.poll() + // If running is false, exit when the queue is empty + if (task == null && !running.get()) break + if (task == null) continue + Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class) + val status = task.execute() + onStatusUpdate(status) } catch (x: Throwable) { onError(task, x) } } + Timber.d("JukeboxMediaPlayer processTasks stopped") } private fun onStatusUpdate(jukeboxStatus: JukeboxStatus) { @@ -621,9 +637,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { queue.add(jukeboxTask) } - @Throws(InterruptedException::class) - fun take(): JukeboxTask { - return queue.take() + fun poll(): JukeboxTask? { + return queue.poll(QUEUE_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS) } fun remove(taskClass: Class) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt index 7ac1b543..44fd7526 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt @@ -36,14 +36,6 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player { TODO("Not yet implemented") } - override fun setMediaItems( - mediaItems: MutableList, - startIndex: Int, - startPositionMs: Long - ) { - TODO("Not yet implemented") - } - override fun setMediaItem(mediaItem: MediaItem) { TODO("Not yet implemented") } 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 80a7b6f2..64aee927 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -33,6 +33,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.playback.PlaybackService import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1 @@ -162,9 +163,28 @@ class MediaPlayerController( switchToLocalPlayer(onCreated) } - rxBusSubscription += RxBus.activeServerChangeObservable.subscribe { - // Update the Jukebox state when the active server has changed - isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault + rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer -> + if (oldServer != OFFLINE_DB_ID) { + // When the server changes, the playlist can retain the downloaded songs. + // Incomplete songs should be removed as the new server won't recognise them. + removeIncompleteTracksFromPlaylist() + DownloadService.requestStop() + } + if (controller is JukeboxMediaPlayer) { + // When the server changes, the Jukebox should be released. + // The new server won't understand the jukebox requests of the old one. + releaseJukebox(controller) + controller = null + } + } + + rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { + val jukebox = activeServerProvider.getActiveServer().jukeboxByDefault + // Remove all songs when changing servers before turning on Jukebox. + // Jukebox wouldn't find the songs on the new server. + if (jukebox) controller?.clearMediaItems() + // Update the Jukebox state as the new server requires + isJukeboxEnabled = jukebox } rxBusSubscription += RxBus.throttledPlaylistObservable.subscribe { @@ -550,7 +570,7 @@ class MediaPlayerController( } private fun switchToJukebox(onCreated: () -> Unit) { - if (JukeboxMediaPlayer.running.get()) return + if (controller is JukeboxMediaPlayer) return val currentPlaylist = playlist val currentIndex = controller?.currentMediaItemIndex ?: 0 val currentPosition = controller?.currentPosition ?: 0 @@ -564,14 +584,14 @@ class MediaPlayerController( Handler(Looper.getMainLooper()).postDelayed({ if (oldController != null) releaseLocalPlayer(oldController) setupJukebox { - controller?.addMediaItems(0, currentPlaylist) - controller?.seekTo(currentIndex, currentPosition) + controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) onCreated() } }, CONTROLLER_SWITCH_DELAY) } private fun switchToLocalPlayer(onCreated: () -> Unit) { + if (controller is MediaController) return val currentPlaylist = playlist val currentIndex = controller?.currentMediaItemIndex ?: 0 val currentPosition = controller?.currentPosition ?: 0 @@ -582,8 +602,7 @@ class MediaPlayerController( Handler(Looper.getMainLooper()).postDelayed({ if (oldController != null) releaseJukebox(oldController) setupLocalPlayer { - controller?.addMediaItems(0, currentPlaylist) - controller?.seekTo(currentIndex, currentPosition) + controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) onCreated() } }, CONTROLLER_SWITCH_DELAY) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index cc30ed5e..48a1061f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -20,10 +20,17 @@ class RxBus { private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper()) - var activeServerChangePublisher: PublishSubject = + var activeServerChangingPublisher: PublishSubject = PublishSubject.create() - var activeServerChangeObservable: Observable = - activeServerChangePublisher.observeOn(mainThread()) + + // Subscribers should be called synchronously, not on another thread + var activeServerChangingObservable: Observable = + activeServerChangingPublisher + + var activeServerChangedPublisher: PublishSubject = + PublishSubject.create() + var activeServerChangedObservable: Observable = + activeServerChangedPublisher.observeOn(mainThread()) val themeChangedEventPublisher: PublishSubject = PublishSubject.create() 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 511039f5..965af68b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -11,6 +11,8 @@ import android.Manifest.permission.POST_NOTIFICATIONS import android.annotation.SuppressLint import android.app.Activity import android.app.PendingIntent +import android.app.Service +import android.app.Service.STOP_FOREGROUND_REMOVE import android.content.ContentResolver import android.content.Context import android.content.Intent @@ -33,6 +35,7 @@ import android.util.TypedValue import android.view.Gravity import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.AnyRes import androidx.appcompat.app.AppCompatActivity @@ -512,7 +515,7 @@ object Util { } } - fun ensurePermissionToPostNotification(fragment: AppCompatActivity) { + fun ensurePermissionToPostNotification(fragment: ComponentActivity) { if (ContextCompat.checkSelfPermission( applicationContext(), POST_NOTIFICATIONS, @@ -751,4 +754,13 @@ object Util { // Ignored } } + + fun Service.stopForegroundRemoveNotification() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + @Suppress("DEPRECATION") + stopForeground(true) + } + } }