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:
parent
6da83db9df
commit
3ca25ed1c6
|
@ -2,8 +2,6 @@
|
|||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background</ID>
|
||||
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<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("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: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>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array<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<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:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
|
||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>
|
||||
|
|
|
@ -40,7 +40,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 io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
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.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.InfoDialog
|
||||
|
@ -83,8 +84,8 @@ class NavigationActivity : AppCompatActivity() {
|
|||
private var headerBackgroundImage: ImageView? = null
|
||||
|
||||
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 lifecycleSupport: MediaPlayerLifecycleSupport by inject()
|
||||
|
@ -181,25 +182,25 @@ class NavigationActivity : AppCompatActivity() {
|
|||
hideNowPlaying()
|
||||
}
|
||||
|
||||
playerStateSubscription = RxBus.playerStateObservable.subscribe {
|
||||
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
||||
if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED)
|
||||
showNowPlaying()
|
||||
else
|
||||
hideNowPlaying()
|
||||
}
|
||||
|
||||
themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe {
|
||||
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
||||
recreate()
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
|
||||
updateNavigationHeaderForServer()
|
||||
}
|
||||
|
||||
serverRepository.liveServerCount().observe(this) { count ->
|
||||
cachedServerCount = count ?: 0
|
||||
updateNavigationHeaderForServer()
|
||||
}
|
||||
|
||||
ActiveServerProvider.liveActiveServerId.observe(this) {
|
||||
updateNavigationHeaderForServer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNavigationHeaderForServer() {
|
||||
|
@ -239,8 +240,7 @@ class NavigationActivity : AppCompatActivity() {
|
|||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
themeChangedEventSubscription?.dispose()
|
||||
playerStateSubscription?.dispose()
|
||||
rxBusSubscription.dispose()
|
||||
imageLoaderProvider.clearImageLoader()
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ import org.moire.ultrasonic.util.Util
|
|||
*/
|
||||
internal class ServerRowAdapter(
|
||||
private var context: Context,
|
||||
private var data: Array<ServerSetting>,
|
||||
passedData: Array<ServerSetting>,
|
||||
private val model: ServerSettingsModel,
|
||||
private val activeServerProvider: ActiveServerProvider,
|
||||
private val manageMode: Boolean,
|
||||
|
@ -38,6 +38,12 @@ internal class ServerRowAdapter(
|
|||
private val serverEditRequestedCallback: ((Int) -> Unit)
|
||||
) : BaseAdapter() {
|
||||
|
||||
private var data: MutableList<ServerSetting> = mutableListOf()
|
||||
|
||||
init {
|
||||
setData(passedData)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MENU_ID_EDIT = 1
|
||||
private const val MENU_ID_DELETE = 2
|
||||
|
@ -49,12 +55,19 @@ internal class ServerRowAdapter(
|
|||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return if (manageMode) data.size else data.size + 1
|
||||
return data.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any {
|
||||
|
@ -69,11 +82,11 @@ internal class ServerRowAdapter(
|
|||
* Creates the Row representation of a Server Setting
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
|
||||
var index = position
|
||||
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
||||
var position = pos
|
||||
|
||||
// Skip "Offline" in manage mode
|
||||
if (manageMode) index++
|
||||
if (manageMode) position++
|
||||
|
||||
var vi: View? = convertView
|
||||
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 image = vi?.findViewById<ImageView>(R.id.server_image)
|
||||
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 ?: ""
|
||||
description?.text = setting?.url ?: ""
|
||||
if (setting == null) serverMenu?.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
val icon: Drawable?
|
||||
val background: Drawable?
|
||||
|
||||
// Configure icons for the row
|
||||
if (index == 0) {
|
||||
if (setting?.id == ActiveServerProvider.OFFLINE_DB_ID) {
|
||||
serverMenu?.visibility = View.INVISIBLE
|
||||
icon = Util.getDrawableFromAttribute(context, R.attr.screen_on_off)
|
||||
background = ContextCompat.getDrawable(context, R.drawable.circle)
|
||||
|
@ -116,7 +124,7 @@ internal class ServerRowAdapter(
|
|||
image?.background = 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)
|
||||
} else {
|
||||
layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple)
|
||||
|
@ -128,7 +136,7 @@ internal class ServerRowAdapter(
|
|||
R.drawable.select_ripple_circle
|
||||
)
|
||||
|
||||
serverMenu?.setOnClickListener { view -> serverMenuClick(view, index) }
|
||||
serverMenu?.setOnClickListener { view -> serverMenuClick(view, position) }
|
||||
|
||||
return vi
|
||||
}
|
||||
|
@ -192,7 +200,8 @@ internal class ServerRowAdapter(
|
|||
return true
|
||||
}
|
||||
MENU_ID_DELETE -> {
|
||||
serverDeletedCallback.invoke(position)
|
||||
val server = getItem(position) as ServerSetting
|
||||
serverDeletedCallback.invoke(server.id)
|
||||
return true
|
||||
}
|
||||
MENU_ID_UP -> {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.moire.ultrasonic.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.Room
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -11,6 +10,7 @@ import org.moire.ultrasonic.R
|
|||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.di.DB_FILENAME
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
@ -52,12 +52,32 @@ class ActiveServerProvider(
|
|||
}
|
||||
|
||||
// Fallback to Offline
|
||||
setActiveServerId(OFFLINE_DB_ID)
|
||||
setActiveServerById(OFFLINE_DB_ID)
|
||||
}
|
||||
|
||||
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
|
||||
* @param index: The index of the Active Server in the Server Selector List
|
||||
|
@ -66,13 +86,13 @@ class ActiveServerProvider(
|
|||
Timber.d("setActiveServerByIndex $index")
|
||||
if (index <= OFFLINE_DB_INDEX) {
|
||||
// Offline mode is selected
|
||||
setActiveServerId(OFFLINE_DB_ID)
|
||||
setActiveServerById(OFFLINE_DB_ID)
|
||||
return
|
||||
}
|
||||
|
||||
launch {
|
||||
val serverId = repository.findByIndex(index)?.id ?: 0
|
||||
setActiveServerId(serverId)
|
||||
setActiveServerById(serverId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,8 +200,6 @@ class ActiveServerProvider(
|
|||
minimumApiVersion = null
|
||||
)
|
||||
|
||||
val liveActiveServerId: MutableLiveData<Int> = MutableLiveData(getActiveServerId())
|
||||
|
||||
/**
|
||||
* Queries if the Active Server is the "Offline" mode of Ultrasonic
|
||||
* @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()
|
||||
|
||||
Settings.activeServer = serverId
|
||||
liveActiveServerId.postValue(serverId)
|
||||
|
||||
Timber.i("setActiveServerById done, new id: %s", serverId)
|
||||
RxBus.activeServerChangePublisher.onNext(serverId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,14 +9,12 @@ import android.widget.ListView
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
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.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.ServerRowAdapter
|
||||
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.model.ServerSettingsModel
|
||||
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
|
||||
*
|
||||
* TODO: Manage mode is unused. Remove it...
|
||||
*/
|
||||
class ServerSelectorFragment : Fragment() {
|
||||
companion object {
|
||||
|
@ -59,6 +59,7 @@ class ServerSelectorFragment : Fragment() {
|
|||
SERVER_SELECTOR_MANAGE_MODE,
|
||||
false
|
||||
) ?: false
|
||||
|
||||
if (manageMode) {
|
||||
FragmentTitle.setTitle(this, R.string.settings_server_manage_servers)
|
||||
} else {
|
||||
|
@ -72,31 +73,26 @@ class ServerSelectorFragment : Fragment() {
|
|||
serverSettingsModel,
|
||||
activeServerProvider,
|
||||
manageMode,
|
||||
{
|
||||
i ->
|
||||
onServerDeleted(i)
|
||||
},
|
||||
{
|
||||
i ->
|
||||
editServer(i)
|
||||
}
|
||||
::deleteServerById,
|
||||
::editServerByIndex
|
||||
)
|
||||
|
||||
listView?.adapter = serverRowAdapter
|
||||
|
||||
listView?.onItemClickListener = AdapterView.OnItemClickListener {
|
||||
_, _, position, _ ->
|
||||
listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ ->
|
||||
|
||||
val server = parent.getItemAtPosition(position) as ServerSetting
|
||||
if (manageMode) {
|
||||
editServer(position + 1)
|
||||
editServerByIndex(position + 1)
|
||||
} else {
|
||||
setActiveServer(position)
|
||||
setActiveServerById(server.id)
|
||||
findNavController().popBackStack(R.id.mainFragment, false)
|
||||
}
|
||||
}
|
||||
|
||||
val fab = view.findViewById<FloatingActionButton>(R.id.server_add_fab)
|
||||
fab.setOnClickListener {
|
||||
editServer(-1)
|
||||
editServerByIndex(-1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,44 +109,37 @@ class ServerSelectorFragment : Fragment() {
|
|||
/**
|
||||
* Sets the active server when a list item is clicked
|
||||
*/
|
||||
private fun setActiveServer(index: 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 {
|
||||
private fun setActiveServerById(id: Int) {
|
||||
|
||||
controller.clearIncomplete()
|
||||
withContext(Dispatchers.IO) {
|
||||
if (activeServerProvider.getActiveServer().index != index) {
|
||||
activeServerProvider.setActiveServerByIndex(index)
|
||||
|
||||
if (activeServerProvider.getActiveServer().id != id) {
|
||||
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
|
||||
*/
|
||||
private fun onServerDeleted(index: Int) {
|
||||
private fun deleteServerById(id: Int) {
|
||||
ErrorDialog.Builder(context)
|
||||
.setTitle(R.string.server_menu_delete)
|
||||
.setMessage(R.string.server_selector_delete_confirmation)
|
||||
.setPositiveButton(R.string.common_delete) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
|
||||
val activeServerIndex = activeServerProvider.getActiveServer().index
|
||||
val id = ActiveServerProvider.getActiveServerId()
|
||||
// Get the id of the current active server
|
||||
val activeServerId = ActiveServerProvider.getActiveServerId()
|
||||
|
||||
// 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
|
||||
activeServerProvider.deleteMetaDatabase(id)
|
||||
activeServerProvider.deleteMetaDatabase(activeServerId)
|
||||
|
||||
Timber.i("Server deleted: $index")
|
||||
Timber.i("Server deleted, id: $id")
|
||||
}
|
||||
.setNegativeButton(R.string.common_cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
|
@ -161,7 +150,7 @@ class ServerSelectorFragment : Fragment() {
|
|||
/**
|
||||
* 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()
|
||||
bundle.putInt(EDIT_SERVER_INTENT_INDEX, index)
|
||||
findNavController().navigate(R.id.serverSelectorToEditServer, bundle)
|
||||
|
|
|
@ -12,6 +12,7 @@ import kotlinx.coroutines.runBlocking
|
|||
import org.moire.ultrasonic.R
|
||||
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.data.ServerSetting
|
||||
import org.moire.ultrasonic.data.ServerSettingDao
|
||||
import timber.log.Timber
|
||||
|
@ -30,6 +31,8 @@ class ServerSettingsModel(
|
|||
/**
|
||||
* Retrieves the list of the configured servers from the database.
|
||||
* This function is asynchronous, uses LiveData to provide the Setting.
|
||||
*
|
||||
* It does not include the Offline "server".
|
||||
*/
|
||||
fun getServerList(): LiveData<List<ServerSetting>> {
|
||||
// This check should run before returning any result
|
||||
|
@ -92,14 +95,14 @@ class ServerSettingsModel(
|
|||
/**
|
||||
* Removes a Setting from the database
|
||||
*/
|
||||
fun deleteItem(index: Int) {
|
||||
if (index == 0) return
|
||||
fun deleteItemById(id: Int) {
|
||||
if (id == OFFLINE_DB_ID) return
|
||||
|
||||
viewModelScope.launch {
|
||||
val itemToBeDeleted = repository.findByIndex(index)
|
||||
val itemToBeDeleted = repository.findById(id)
|
||||
if (itemToBeDeleted != null) {
|
||||
repository.delete(itemToBeDeleted)
|
||||
Timber.d("deleteItem deleted index: $index")
|
||||
Timber.d("deleteItem deleted id: $id")
|
||||
reindexSettings()
|
||||
activeServerProvider.invalidateCache()
|
||||
}
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
package org.moire.ultrasonic.playback
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.util.Assertions
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.datasource.BaseDataSource
|
||||
import androidx.media3.datasource.DataSourceException
|
||||
|
@ -43,15 +43,15 @@ import timber.log.Timber
|
|||
* priority) the `dataSpec`, [.setRequestProperty] and the default parameters used to
|
||||
* construct the instance.
|
||||
*/
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@Suppress("MagicNumber")
|
||||
@UnstableApi
|
||||
open class APIDataSource private constructor(
|
||||
subsonicAPIClient: SubsonicAPIClient
|
||||
) : BaseDataSource(true),
|
||||
HttpDataSource {
|
||||
|
||||
/** [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 var transferListener: TransferListener? = null
|
||||
|
||||
|
@ -75,6 +75,10 @@ open class APIDataSource private constructor(
|
|||
return this
|
||||
}
|
||||
|
||||
fun setAPIClient(newClient: SubsonicAPIClient) {
|
||||
this.subsonicAPIClient = newClient
|
||||
}
|
||||
|
||||
override fun createDataSource(): APIDataSource {
|
||||
val dataSource = APIDataSource(
|
||||
subsonicAPIClient
|
||||
|
@ -318,6 +322,7 @@ open class APIDataSource private constructor(
|
|||
return C.RESULT_END_OF_INPUT
|
||||
}
|
||||
bytesRead += read.toLong()
|
||||
// TODO
|
||||
// bytesTransferred(read)
|
||||
return read
|
||||
}
|
||||
|
|
|
@ -29,21 +29,27 @@ import androidx.media3.exoplayer.ExoPlayer
|
|||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.activity.NavigationActivity
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||
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.Settings
|
||||
|
||||
class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var apiDataSource: APIDataSource.Factory
|
||||
private lateinit var dataSourceFactory: DataSource.Factory
|
||||
|
||||
private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback
|
||||
|
||||
private var rxBusSubscription = CompositeDisposable()
|
||||
|
||||
/*
|
||||
* For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
|
||||
* and thereby customarily it is required to rebuild it..
|
||||
|
@ -64,11 +70,18 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
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() {
|
||||
player.release()
|
||||
mediaLibrarySession.release()
|
||||
rxBusSubscription.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
@ -88,8 +101,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||
val subsonicAPIClient: SubsonicAPIClient by inject()
|
||||
|
||||
// Create a MediaSource which passes calls through our OkHttp Stack
|
||||
apiDataSource = 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
|
||||
val renderer = DefaultRenderersFactory(this)
|
||||
|
|
|
@ -18,6 +18,7 @@ import androidx.media3.common.Timeline
|
|||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
|
@ -61,6 +62,8 @@ class MediaPlayerController(
|
|||
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private var sessionToken =
|
||||
SessionToken(context, ComponentName(context, PlaybackService::class.java))
|
||||
|
||||
|
@ -109,6 +112,11 @@ class MediaPlayerController(
|
|||
// controller?.play()
|
||||
}, MoreExecutors.directExecutor())
|
||||
|
||||
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
|
||||
// Update the Jukebox state when the active server has changed
|
||||
isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault
|
||||
}
|
||||
|
||||
created = true
|
||||
Timber.i("MediaPlayerController created")
|
||||
}
|
||||
|
|
|
@ -27,8 +27,14 @@ import org.moire.ultrasonic.data.ActiveServerProvider
|
|||
import org.moire.ultrasonic.di.OFFLINE_MUSIC_SERVICE
|
||||
import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE
|
||||
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 {
|
||||
@JvmStatic
|
||||
fun getMusicService(): MusicService {
|
||||
|
@ -45,6 +51,7 @@ object MusicServiceFactory : KoinComponent {
|
|||
*/
|
||||
@JvmStatic
|
||||
fun resetMusicService() {
|
||||
Timber.i("Regenerating Koin Music Service Module")
|
||||
unloadKoinModules(musicServiceModule)
|
||||
loadKoinModules(musicServiceModule)
|
||||
}
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
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.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.moire.ultrasonic.domain.PlayerState
|
||||
|
||||
class RxBus {
|
||||
companion object {
|
||||
var mediaSessionTokenPublisher: PublishSubject<MediaSessionCompat.Token> =
|
||||
|
||||
var activeServerChangePublisher: PublishSubject<Int> =
|
||||
PublishSubject.create()
|
||||
val mediaSessionTokenObservable: Observable<MediaSessionCompat.Token> =
|
||||
mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
.replay(1)
|
||||
.autoConnect(0)
|
||||
var activeServerChangeObservable: Observable<Int> =
|
||||
activeServerChangePublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val themeChangedEventPublisher: PublishSubject<Unit> =
|
||||
PublishSubject.create()
|
||||
|
@ -43,38 +39,11 @@ class RxBus {
|
|||
.replay(1)
|
||||
.autoConnect(0)
|
||||
|
||||
val playbackPositionPublisher: PublishSubject<Int> =
|
||||
PublishSubject.create()
|
||||
val playbackPositionObservable: Observable<Int> =
|
||||
playbackPositionPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
.throttleFirst(1, TimeUnit.SECONDS)
|
||||
.replay(1)
|
||||
.autoConnect(0)
|
||||
|
||||
// Commands
|
||||
val dismissNowPlayingCommandPublisher: PublishSubject<Unit> =
|
||||
PublishSubject.create()
|
||||
val dismissNowPlayingCommandObservable: Observable<Unit> =
|
||||
dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val playFromMediaIdCommandPublisher: PublishSubject<Pair<String?, Bundle?>> =
|
||||
PublishSubject.create()
|
||||
val playFromMediaIdCommandObservable: Observable<Pair<String?, Bundle?>> =
|
||||
playFromMediaIdCommandPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val playFromSearchCommandPublisher: PublishSubject<Pair<String?, Bundle?>> =
|
||||
PublishSubject.create()
|
||||
val playFromSearchCommandObservable: Observable<Pair<String?, Bundle?>> =
|
||||
playFromSearchCommandPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val skipToQueueItemCommandPublisher: PublishSubject<Long> =
|
||||
PublishSubject.create()
|
||||
val skipToQueueItemCommandObservable: Observable<Long> =
|
||||
skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
fun releaseMediaSessionToken() {
|
||||
mediaSessionTokenPublisher = PublishSubject.create()
|
||||
}
|
||||
}
|
||||
|
||||
data class StateWithTrack(val state: PlayerState, val track: DownloadFile?, val index: Int = -1)
|
||||
|
|
Loading…
Reference in New Issue