Rework ActiveServer handling.

Remove blocking call on setting the server.
Implement offline server display more cleanly.
Reconfigure the SourceFactory when the active server has changed
This commit is contained in:
tzugen 2022-04-08 18:08:56 +02:00
parent 6da83db9df
commit 3ca25ed1c6
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
11 changed files with 147 additions and 123 deletions

View File

@ -2,8 +2,6 @@
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues/> <ManuallySuppressedIssues/>
<CurrentIssues> <CurrentIssues>
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided>$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID>
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.&lt;no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID> <ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.&lt;no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID> <ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID> <ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
@ -12,7 +10,7 @@
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID> <ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID> <ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID> <ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array&lt;ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID> <ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array&lt;ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID>
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID> <ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID> <ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID> <ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>

View File

@ -40,7 +40,7 @@ import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -54,6 +54,7 @@ import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.InfoDialog
@ -83,8 +84,8 @@ class NavigationActivity : AppCompatActivity() {
private var headerBackgroundImage: ImageView? = null private var headerBackgroundImage: ImageView? = null
private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var appBarConfiguration: AppBarConfiguration
private var themeChangedEventSubscription: Disposable? = null
private var playerStateSubscription: Disposable? = null private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
private val serverSettingsModel: ServerSettingsModel by viewModel() private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
@ -181,25 +182,25 @@ class NavigationActivity : AppCompatActivity() {
hideNowPlaying() hideNowPlaying()
} }
playerStateSubscription = RxBus.playerStateObservable.subscribe { rxBusSubscription += RxBus.playerStateObservable.subscribe {
if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED) if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED)
showNowPlaying() showNowPlaying()
else else
hideNowPlaying() hideNowPlaying()
} }
themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe { rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
recreate() recreate()
} }
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
updateNavigationHeaderForServer()
}
serverRepository.liveServerCount().observe(this) { count -> serverRepository.liveServerCount().observe(this) { count ->
cachedServerCount = count ?: 0 cachedServerCount = count ?: 0
updateNavigationHeaderForServer() updateNavigationHeaderForServer()
} }
ActiveServerProvider.liveActiveServerId.observe(this) {
updateNavigationHeaderForServer()
}
} }
private fun updateNavigationHeaderForServer() { private fun updateNavigationHeaderForServer() {
@ -239,8 +240,7 @@ class NavigationActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
themeChangedEventSubscription?.dispose() rxBusSubscription.dispose()
playerStateSubscription?.dispose()
imageLoaderProvider.clearImageLoader() imageLoaderProvider.clearImageLoader()
} }

View File

@ -30,7 +30,7 @@ import org.moire.ultrasonic.util.Util
*/ */
internal class ServerRowAdapter( internal class ServerRowAdapter(
private var context: Context, private var context: Context,
private var data: Array<ServerSetting>, passedData: Array<ServerSetting>,
private val model: ServerSettingsModel, private val model: ServerSettingsModel,
private val activeServerProvider: ActiveServerProvider, private val activeServerProvider: ActiveServerProvider,
private val manageMode: Boolean, private val manageMode: Boolean,
@ -38,6 +38,12 @@ internal class ServerRowAdapter(
private val serverEditRequestedCallback: ((Int) -> Unit) private val serverEditRequestedCallback: ((Int) -> Unit)
) : BaseAdapter() { ) : BaseAdapter() {
private var data: MutableList<ServerSetting> = mutableListOf()
init {
setData(passedData)
}
companion object { companion object {
private const val MENU_ID_EDIT = 1 private const val MENU_ID_EDIT = 1
private const val MENU_ID_DELETE = 2 private const val MENU_ID_DELETE = 2
@ -49,12 +55,19 @@ internal class ServerRowAdapter(
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
fun setData(data: Array<ServerSetting>) { fun setData(data: Array<ServerSetting>) {
this.data = data this.data.clear()
// In read mode show the offline server as well
if (!manageMode) {
this.data.add(ActiveServerProvider.OFFLINE_DB)
}
this.data.addAll(data)
notifyDataSetChanged() notifyDataSetChanged()
} }
override fun getCount(): Int { override fun getCount(): Int {
return if (manageMode) data.size else data.size + 1 return data.size
} }
override fun getItem(position: Int): Any { override fun getItem(position: Int): Any {
@ -69,11 +82,11 @@ internal class ServerRowAdapter(
* Creates the Row representation of a Server Setting * Creates the Row representation of a Server Setting
*/ */
@Suppress("LongMethod") @Suppress("LongMethod")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
var index = position var position = pos
// Skip "Offline" in manage mode // Skip "Offline" in manage mode
if (manageMode) index++ if (manageMode) position++
var vi: View? = convertView var vi: View? = convertView
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false) if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)
@ -83,22 +96,17 @@ internal class ServerRowAdapter(
val layout = vi?.findViewById<ConstraintLayout>(R.id.server_layout) val layout = vi?.findViewById<ConstraintLayout>(R.id.server_layout)
val image = vi?.findViewById<ImageView>(R.id.server_image) val image = vi?.findViewById<ImageView>(R.id.server_image)
val serverMenu = vi?.findViewById<ImageButton>(R.id.server_menu) val serverMenu = vi?.findViewById<ImageButton>(R.id.server_menu)
val setting = data.singleOrNull { t -> t.index == index } val setting = data.singleOrNull { t -> t.index == position }
if (index == 0) {
text?.text = context.getString(R.string.main_offline)
description?.text = ""
} else {
text?.text = setting?.name ?: "" text?.text = setting?.name ?: ""
description?.text = setting?.url ?: "" description?.text = setting?.url ?: ""
if (setting == null) serverMenu?.visibility = View.INVISIBLE if (setting == null) serverMenu?.visibility = View.INVISIBLE
}
val icon: Drawable? val icon: Drawable?
val background: Drawable? val background: Drawable?
// Configure icons for the row // Configure icons for the row
if (index == 0) { if (setting?.id == ActiveServerProvider.OFFLINE_DB_ID) {
serverMenu?.visibility = View.INVISIBLE serverMenu?.visibility = View.INVISIBLE
icon = Util.getDrawableFromAttribute(context, R.attr.screen_on_off) icon = Util.getDrawableFromAttribute(context, R.attr.screen_on_off)
background = ContextCompat.getDrawable(context, R.drawable.circle) background = ContextCompat.getDrawable(context, R.drawable.circle)
@ -116,7 +124,7 @@ internal class ServerRowAdapter(
image?.background = background image?.background = background
// Highlight the Active Server's row by changing its background // Highlight the Active Server's row by changing its background
if (index == activeServerProvider.getActiveServer().index) { if (position == activeServerProvider.getActiveServer().index) {
layout?.background = ContextCompat.getDrawable(context, R.drawable.select_ripple) layout?.background = ContextCompat.getDrawable(context, R.drawable.select_ripple)
} else { } else {
layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple) layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple)
@ -128,7 +136,7 @@ internal class ServerRowAdapter(
R.drawable.select_ripple_circle R.drawable.select_ripple_circle
) )
serverMenu?.setOnClickListener { view -> serverMenuClick(view, index) } serverMenu?.setOnClickListener { view -> serverMenuClick(view, position) }
return vi return vi
} }
@ -192,7 +200,8 @@ internal class ServerRowAdapter(
return true return true
} }
MENU_ID_DELETE -> { MENU_ID_DELETE -> {
serverDeletedCallback.invoke(position) val server = getItem(position) as ServerSetting
serverDeletedCallback.invoke(server.id)
return true return true
} }
MENU_ID_UP -> { MENU_ID_UP -> {

View File

@ -1,6 +1,5 @@
package org.moire.ultrasonic.data package org.moire.ultrasonic.data
import androidx.lifecycle.MutableLiveData
import androidx.room.Room import androidx.room.Room
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -11,6 +10,7 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.di.DB_FILENAME import org.moire.ultrasonic.di.DB_FILENAME
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -52,12 +52,32 @@ class ActiveServerProvider(
} }
// Fallback to Offline // Fallback to Offline
setActiveServerId(OFFLINE_DB_ID) setActiveServerById(OFFLINE_DB_ID)
} }
return OFFLINE_DB return OFFLINE_DB
} }
/**
* Resolves the index (sort order) of a server to its id (unique)
* @param index: The index of the server in the server selector
* @return id: The unique id of the server
*/
fun getServerIdFromIndex(index: Int): Int {
if (index <= OFFLINE_DB_INDEX) {
// Offline mode is selected
return OFFLINE_DB_ID
}
var id: Int
runBlocking {
id = repository.findByIndex(index)?.id ?: 0
}
return id
}
/** /**
* Sets the Active Server by the Server Index in the Server Selector List * Sets the Active Server by the Server Index in the Server Selector List
* @param index: The index of the Active Server in the Server Selector List * @param index: The index of the Active Server in the Server Selector List
@ -66,13 +86,13 @@ class ActiveServerProvider(
Timber.d("setActiveServerByIndex $index") Timber.d("setActiveServerByIndex $index")
if (index <= OFFLINE_DB_INDEX) { if (index <= OFFLINE_DB_INDEX) {
// Offline mode is selected // Offline mode is selected
setActiveServerId(OFFLINE_DB_ID) setActiveServerById(OFFLINE_DB_ID)
return return
} }
launch { launch {
val serverId = repository.findByIndex(index)?.id ?: 0 val serverId = repository.findByIndex(index)?.id ?: 0
setActiveServerId(serverId) setActiveServerById(serverId)
} }
} }
@ -180,8 +200,6 @@ class ActiveServerProvider(
minimumApiVersion = null minimumApiVersion = null
) )
val liveActiveServerId: MutableLiveData<Int> = MutableLiveData(getActiveServerId())
/** /**
* Queries if the Active Server is the "Offline" mode of Ultrasonic * Queries if the Active Server is the "Offline" mode of Ultrasonic
* @return True, if the "Offline" mode is selected * @return True, if the "Offline" mode is selected
@ -198,13 +216,16 @@ class ActiveServerProvider(
} }
/** /**
* Sets the Id of the Active Server * Sets the Active Server by its unique id
* @param serverId: The id of the desired server
*/ */
fun setActiveServerId(serverId: Int) { fun setActiveServerById(serverId: Int) {
resetMusicService() resetMusicService()
Settings.activeServer = serverId Settings.activeServer = serverId
liveActiveServerId.postValue(serverId)
Timber.i("setActiveServerById done, new id: %s", serverId)
RxBus.activeServerChangePublisher.onNext(serverId)
} }
/** /**

View File

@ -9,14 +9,12 @@ import android.widget.ListView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ServerRowAdapter import org.moire.ultrasonic.adapters.ServerRowAdapter
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX
import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerController
@ -26,6 +24,8 @@ import timber.log.Timber
/** /**
* Displays the list of configured servers, they can be selected or edited * Displays the list of configured servers, they can be selected or edited
*
* TODO: Manage mode is unused. Remove it...
*/ */
class ServerSelectorFragment : Fragment() { class ServerSelectorFragment : Fragment() {
companion object { companion object {
@ -59,6 +59,7 @@ class ServerSelectorFragment : Fragment() {
SERVER_SELECTOR_MANAGE_MODE, SERVER_SELECTOR_MANAGE_MODE,
false false
) ?: false ) ?: false
if (manageMode) { if (manageMode) {
FragmentTitle.setTitle(this, R.string.settings_server_manage_servers) FragmentTitle.setTitle(this, R.string.settings_server_manage_servers)
} else { } else {
@ -72,31 +73,26 @@ class ServerSelectorFragment : Fragment() {
serverSettingsModel, serverSettingsModel,
activeServerProvider, activeServerProvider,
manageMode, manageMode,
{ ::deleteServerById,
i -> ::editServerByIndex
onServerDeleted(i)
},
{
i ->
editServer(i)
}
) )
listView?.adapter = serverRowAdapter listView?.adapter = serverRowAdapter
listView?.onItemClickListener = AdapterView.OnItemClickListener { listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ ->
_, _, position, _ ->
val server = parent.getItemAtPosition(position) as ServerSetting
if (manageMode) { if (manageMode) {
editServer(position + 1) editServerByIndex(position + 1)
} else { } else {
setActiveServer(position) setActiveServerById(server.id)
findNavController().popBackStack(R.id.mainFragment, false) findNavController().popBackStack(R.id.mainFragment, false)
} }
} }
val fab = view.findViewById<FloatingActionButton>(R.id.server_add_fab) val fab = view.findViewById<FloatingActionButton>(R.id.server_add_fab)
fab.setOnClickListener { fab.setOnClickListener {
editServer(-1) editServerByIndex(-1)
} }
} }
@ -113,44 +109,37 @@ class ServerSelectorFragment : Fragment() {
/** /**
* Sets the active server when a list item is clicked * Sets the active server when a list item is clicked
*/ */
private fun setActiveServer(index: Int) { private fun setActiveServerById(id: Int) {
// TODO this is still a blocking call - we shouldn't leave this activity before the active server is updated.
// Maybe this can be refactored by using LiveData, or this can be made more user friendly with a ProgressDialog
runBlocking {
controller.clearIncomplete() controller.clearIncomplete()
withContext(Dispatchers.IO) {
if (activeServerProvider.getActiveServer().index != index) { if (activeServerProvider.getActiveServer().id != id) {
activeServerProvider.setActiveServerByIndex(index) ActiveServerProvider.setActiveServerById(id)
} }
} }
controller.isJukeboxEnabled =
activeServerProvider.getActiveServer().jukeboxByDefault
}
Timber.i("Active server was set to: $index")
}
/** /**
* This Callback handles the deletion of a Server Setting * This Callback handles the deletion of a Server Setting
*/ */
private fun onServerDeleted(index: Int) { private fun deleteServerById(id: Int) {
ErrorDialog.Builder(context) ErrorDialog.Builder(context)
.setTitle(R.string.server_menu_delete) .setTitle(R.string.server_menu_delete)
.setMessage(R.string.server_selector_delete_confirmation) .setMessage(R.string.server_selector_delete_confirmation)
.setPositiveButton(R.string.common_delete) { dialog, _ -> .setPositiveButton(R.string.common_delete) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
val activeServerIndex = activeServerProvider.getActiveServer().index // Get the id of the current active server
val id = ActiveServerProvider.getActiveServerId() val activeServerId = ActiveServerProvider.getActiveServerId()
// If the currently active server is deleted, go offline // If the currently active server is deleted, go offline
if (index == activeServerIndex) setActiveServer(-1) if (id == activeServerId) setActiveServerById(ActiveServerProvider.OFFLINE_DB_ID)
serverSettingsModel.deleteItem(index) serverSettingsModel.deleteItemById(id)
// Clear the metadata cache // Clear the metadata cache
activeServerProvider.deleteMetaDatabase(id) activeServerProvider.deleteMetaDatabase(activeServerId)
Timber.i("Server deleted: $index") Timber.i("Server deleted, id: $id")
} }
.setNegativeButton(R.string.common_cancel) { dialog, _ -> .setNegativeButton(R.string.common_cancel) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
@ -161,7 +150,7 @@ class ServerSelectorFragment : Fragment() {
/** /**
* Starts the Edit Server Fragment to edit the details of a server * Starts the Edit Server Fragment to edit the details of a server
*/ */
private fun editServer(index: Int) { private fun editServerByIndex(index: Int) {
val bundle = Bundle() val bundle = Bundle()
bundle.putInt(EDIT_SERVER_INTENT_INDEX, index) bundle.putInt(EDIT_SERVER_INTENT_INDEX, index)
findNavController().navigate(R.id.serverSelectorToEditServer, bundle) findNavController().navigate(R.id.serverSelectorToEditServer, bundle)

View File

@ -12,6 +12,7 @@ import kotlinx.coroutines.runBlocking
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider 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.data.ServerSetting
import org.moire.ultrasonic.data.ServerSettingDao import org.moire.ultrasonic.data.ServerSettingDao
import timber.log.Timber import timber.log.Timber
@ -30,6 +31,8 @@ class ServerSettingsModel(
/** /**
* Retrieves the list of the configured servers from the database. * Retrieves the list of the configured servers from the database.
* This function is asynchronous, uses LiveData to provide the Setting. * This function is asynchronous, uses LiveData to provide the Setting.
*
* It does not include the Offline "server".
*/ */
fun getServerList(): LiveData<List<ServerSetting>> { fun getServerList(): LiveData<List<ServerSetting>> {
// This check should run before returning any result // This check should run before returning any result
@ -92,14 +95,14 @@ class ServerSettingsModel(
/** /**
* Removes a Setting from the database * Removes a Setting from the database
*/ */
fun deleteItem(index: Int) { fun deleteItemById(id: Int) {
if (index == 0) return if (id == OFFLINE_DB_ID) return
viewModelScope.launch { viewModelScope.launch {
val itemToBeDeleted = repository.findByIndex(index) val itemToBeDeleted = repository.findById(id)
if (itemToBeDeleted != null) { if (itemToBeDeleted != null) {
repository.delete(itemToBeDeleted) repository.delete(itemToBeDeleted)
Timber.d("deleteItem deleted index: $index") Timber.d("deleteItem deleted id: $id")
reindexSettings() reindexSettings()
activeServerProvider.invalidateCache() activeServerProvider.invalidateCache()
} }

View File

@ -7,12 +7,12 @@
package org.moire.ultrasonic.playback package org.moire.ultrasonic.playback
import android.annotation.SuppressLint
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.util.Assertions import androidx.media3.common.util.Assertions
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util import androidx.media3.common.util.Util
import androidx.media3.datasource.BaseDataSource import androidx.media3.datasource.BaseDataSource
import androidx.media3.datasource.DataSourceException import androidx.media3.datasource.DataSourceException
@ -43,15 +43,15 @@ import timber.log.Timber
* priority) the `dataSpec`, [.setRequestProperty] and the default parameters used to * priority) the `dataSpec`, [.setRequestProperty] and the default parameters used to
* construct the instance. * construct the instance.
*/ */
@SuppressLint("UnsafeOptInUsageError")
@Suppress("MagicNumber") @Suppress("MagicNumber")
@UnstableApi
open class APIDataSource private constructor( open class APIDataSource private constructor(
subsonicAPIClient: SubsonicAPIClient subsonicAPIClient: SubsonicAPIClient
) : BaseDataSource(true), ) : BaseDataSource(true),
HttpDataSource { HttpDataSource {
/** [DataSource.Factory] for [APIDataSource] instances. */ /** [DataSource.Factory] for [APIDataSource] instances. */
class Factory(private val subsonicAPIClient: SubsonicAPIClient) : HttpDataSource.Factory { class Factory(private var subsonicAPIClient: SubsonicAPIClient) : HttpDataSource.Factory {
private val defaultRequestProperties: RequestProperties = RequestProperties() private val defaultRequestProperties: RequestProperties = RequestProperties()
private var transferListener: TransferListener? = null private var transferListener: TransferListener? = null
@ -75,6 +75,10 @@ open class APIDataSource private constructor(
return this return this
} }
fun setAPIClient(newClient: SubsonicAPIClient) {
this.subsonicAPIClient = newClient
}
override fun createDataSource(): APIDataSource { override fun createDataSource(): APIDataSource {
val dataSource = APIDataSource( val dataSource = APIDataSource(
subsonicAPIClient subsonicAPIClient
@ -318,6 +322,7 @@ open class APIDataSource private constructor(
return C.RESULT_END_OF_INPUT return C.RESULT_END_OF_INPUT
} }
bytesRead += read.toLong() bytesRead += read.toLong()
// TODO
// bytesTransferred(read) // bytesTransferred(read)
return read return read
} }

View File

@ -29,21 +29,27 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
class PlaybackService : MediaLibraryService(), KoinComponent { class PlaybackService : MediaLibraryService(), KoinComponent {
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var apiDataSource: APIDataSource.Factory
private lateinit var dataSourceFactory: DataSource.Factory private lateinit var dataSourceFactory: DataSource.Factory
private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback
private var rxBusSubscription = CompositeDisposable()
/* /*
* For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
* and thereby customarily it is required to rebuild it.. * and thereby customarily it is required to rebuild it..
@ -64,11 +70,18 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initializeSessionAndPlayer() initializeSessionAndPlayer()
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
// Update the API endpoint when the active server has changed
val newClient: SubsonicAPIClient by inject()
apiDataSource.setAPIClient(newClient)
}
} }
override fun onDestroy() { override fun onDestroy() {
player.release() player.release()
mediaLibrarySession.release() mediaLibrarySession.release()
rxBusSubscription.dispose()
super.onDestroy() super.onDestroy()
} }
@ -88,8 +101,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
val subsonicAPIClient: SubsonicAPIClient by inject() val subsonicAPIClient: SubsonicAPIClient by inject()
// Create a MediaSource which passes calls through our OkHttp Stack // Create a MediaSource which passes calls through our OkHttp Stack
apiDataSource = APIDataSource.Factory(subsonicAPIClient)
dataSourceFactory = APIDataSource.Factory(subsonicAPIClient) dataSourceFactory = APIDataSource.Factory(subsonicAPIClient)
val cacheDataSourceFactory: DataSource.Factory = CachedDataSource.Factory(dataSourceFactory) val cacheDataSourceFactory: DataSource.Factory = CachedDataSource.Factory(apiDataSource)
// Create a renderer with HW rendering support // Create a renderer with HW rendering support
val renderer = DefaultRenderersFactory(this) val renderer = DefaultRenderersFactory(this)

View File

@ -18,6 +18,7 @@ import androidx.media3.common.Timeline
import androidx.media3.session.MediaController import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.MoreExecutors
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
@ -61,6 +62,8 @@ class MediaPlayerController(
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
private var sessionToken = private var sessionToken =
SessionToken(context, ComponentName(context, PlaybackService::class.java)) SessionToken(context, ComponentName(context, PlaybackService::class.java))
@ -109,6 +112,11 @@ class MediaPlayerController(
// controller?.play() // controller?.play()
}, MoreExecutors.directExecutor()) }, MoreExecutors.directExecutor())
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
// Update the Jukebox state when the active server has changed
isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault
}
created = true created = true
Timber.i("MediaPlayerController created") Timber.i("MediaPlayerController created")
} }

View File

@ -27,8 +27,14 @@ import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.di.OFFLINE_MUSIC_SERVICE import org.moire.ultrasonic.di.OFFLINE_MUSIC_SERVICE
import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE
import org.moire.ultrasonic.di.musicServiceModule import org.moire.ultrasonic.di.musicServiceModule
import timber.log.Timber
// TODO Refactor everywhere to use DI way to get MusicService, and then remove this class /*
* TODO: When resetMusicService is called, a large number of classes are completely newly instantiated,
* which take quite a bit of time.
*
* Instead it would probably be faster to listen to Rx
*/
object MusicServiceFactory : KoinComponent { object MusicServiceFactory : KoinComponent {
@JvmStatic @JvmStatic
fun getMusicService(): MusicService { fun getMusicService(): MusicService {
@ -45,6 +51,7 @@ object MusicServiceFactory : KoinComponent {
*/ */
@JvmStatic @JvmStatic
fun resetMusicService() { fun resetMusicService() {
Timber.i("Regenerating Koin Music Service Module")
unloadKoinModules(musicServiceModule) unloadKoinModules(musicServiceModule)
loadKoinModules(musicServiceModule) loadKoinModules(musicServiceModule)
} }

View File

@ -1,23 +1,19 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.os.Bundle
import android.support.v4.media.session.MediaSessionCompat
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
import java.util.concurrent.TimeUnit
import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.PlayerState
class RxBus { class RxBus {
companion object { companion object {
var mediaSessionTokenPublisher: PublishSubject<MediaSessionCompat.Token> =
var activeServerChangePublisher: PublishSubject<Int> =
PublishSubject.create() PublishSubject.create()
val mediaSessionTokenObservable: Observable<MediaSessionCompat.Token> = var activeServerChangeObservable: Observable<Int> =
mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread()) activeServerChangePublisher.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect(0)
val themeChangedEventPublisher: PublishSubject<Unit> = val themeChangedEventPublisher: PublishSubject<Unit> =
PublishSubject.create() PublishSubject.create()
@ -43,38 +39,11 @@ class RxBus {
.replay(1) .replay(1)
.autoConnect(0) .autoConnect(0)
val playbackPositionPublisher: PublishSubject<Int> =
PublishSubject.create()
val playbackPositionObservable: Observable<Int> =
playbackPositionPublisher.observeOn(AndroidSchedulers.mainThread())
.throttleFirst(1, TimeUnit.SECONDS)
.replay(1)
.autoConnect(0)
// Commands // Commands
val dismissNowPlayingCommandPublisher: PublishSubject<Unit> = val dismissNowPlayingCommandPublisher: PublishSubject<Unit> =
PublishSubject.create() PublishSubject.create()
val dismissNowPlayingCommandObservable: Observable<Unit> = val dismissNowPlayingCommandObservable: Observable<Unit> =
dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread())
val playFromMediaIdCommandPublisher: PublishSubject<Pair<String?, Bundle?>> =
PublishSubject.create()
val playFromMediaIdCommandObservable: Observable<Pair<String?, Bundle?>> =
playFromMediaIdCommandPublisher.observeOn(AndroidSchedulers.mainThread())
val playFromSearchCommandPublisher: PublishSubject<Pair<String?, Bundle?>> =
PublishSubject.create()
val playFromSearchCommandObservable: Observable<Pair<String?, Bundle?>> =
playFromSearchCommandPublisher.observeOn(AndroidSchedulers.mainThread())
val skipToQueueItemCommandPublisher: PublishSubject<Long> =
PublishSubject.create()
val skipToQueueItemCommandObservable: Observable<Long> =
skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread())
fun releaseMediaSessionToken() {
mediaSessionTokenPublisher = PublishSubject.create()
}
} }
data class StateWithTrack(val state: PlayerState, val track: DownloadFile?, val index: Int = -1) data class StateWithTrack(val state: PlayerState, val track: DownloadFile?, val index: Int = -1)