all: rework context-dependent object use

Rework some of the taped together ways context-dependent objects were
replied on in-app, such as removing redundant constructs and extremely
hacky lifecycle mechanisms.
This commit is contained in:
Alexander Capehart 2022-12-31 11:11:09 -07:00
parent bf56a50b59
commit 493b0a9f32
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
30 changed files with 175 additions and 232 deletions

View file

@ -50,7 +50,6 @@ audio focus was lost
#### What's Changed #### What's Changed
- Ignore MediaStore tags is now Auxio's default and unchangeable behavior. The option has been removed. - Ignore MediaStore tags is now Auxio's default and unchangeable behavior. The option has been removed.
- Removed the "Play from genre" option in the library/detail playback mode settings+
- "Use alternate notification action" is now "Custom notification action" - "Use alternate notification action" is now "Custom notification action"
- "Show covers" and "Ignore MediaStore covers" have been unified into "Album covers" - "Show covers" and "Ignore MediaStore covers" have been unified into "Album covers"

View file

@ -61,10 +61,8 @@ class MainFragment :
private val selectionModel: SelectionViewModel by activityViewModels() private val selectionModel: SelectionViewModel by activityViewModels()
private val callback = DynamicBackPressedCallback() private val callback = DynamicBackPressedCallback()
private var lastInsets: WindowInsets? = null private var lastInsets: WindowInsets? = null
private var elevationNormal = 0f
private var initialNavDestinationChange = true private var initialNavDestinationChange = true
private val elevationNormal: Float by lifecycleObject { binding ->
binding.context.getDimen(R.dimen.elevation_normal)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -77,6 +75,8 @@ class MainFragment :
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
elevationNormal = binding.context.getDimen(R.dimen.elevation_normal)
// --- UI SETUP --- // --- UI SETUP ---
val context = requireActivity() val context = requireActivity()
// Override the back pressed listener so we can map back navigation to collapsing // Override the back pressed listener so we can map back navigation to collapsing

View file

@ -130,11 +130,11 @@ class DetailViewModel(application: Application) :
} }
init { init {
musicStore.addCallback(this) musicStore.addListener(this)
} }
override fun onCleared() { override fun onCleared() {
musicStore.removeCallback(this) musicStore.removeListener(this)
} }
override fun onLibraryChanged(library: MusicStore.Library?) { override fun onLibraryChanged(library: MusicStore.Library?) {

View file

@ -72,17 +72,7 @@ class HomeFragment :
private val homeModel: HomeViewModel by androidActivityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels()
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
// lifecycleObject builds this in the creation step, so doing this is okay.
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
musicModel.refresh()
}
}
private val sortItem: MenuItem by lifecycleObject { binding ->
binding.homeToolbar.menu.findItem(R.id.submenu_sorting)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -105,6 +95,12 @@ class HomeFragment :
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
// Have to set up the permission launcher before the view is shown
storagePermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
musicModel.refresh()
}
// --- UI SETUP --- // --- UI SETUP ---
binding.homeAppbar.addOnOffsetChangedListener(this) binding.homeAppbar.addOnOffsetChangedListener(this)
binding.homeToolbar.setOnMenuItemClickListener(this) binding.homeToolbar.setOnMenuItemClickListener(this)
@ -171,6 +167,7 @@ class HomeFragment :
override fun onDestroyBinding(binding: FragmentHomeBinding) { override fun onDestroyBinding(binding: FragmentHomeBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
storagePermissionLauncher = null
binding.homeAppbar.removeOnOffsetChangedListener(this) binding.homeAppbar.removeOnOffsetChangedListener(this)
binding.homeToolbar.setOnMenuItemClickListener(null) binding.homeToolbar.setOnMenuItemClickListener(null)
} }
@ -285,7 +282,9 @@ class HomeFragment :
} }
} }
val sortMenu = requireNotNull(sortItem.subMenu) val sortMenu =
unlikelyToBeNull(
requireBinding().homeToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
val toHighlight = homeModel.getSortForTab(tabMode) val toHighlight = homeModel.getSortForTab(tabMode)
for (option in sortMenu) { for (option in sortMenu) {
@ -374,7 +373,10 @@ class HomeFragment :
visibility = View.VISIBLE visibility = View.VISIBLE
text = context.getString(R.string.lbl_grant) text = context.getString(R.string.lbl_grant)
setOnClickListener { setOnClickListener {
storagePermissionLauncher.launch(Indexer.PERMISSION_READ_AUDIO) requireNotNull(storagePermissionLauncher) {
"Permission launcher was not available"
}
.launch(Indexer.PERMISSION_READ_AUDIO)
} }
} }
} }

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.home package org.oxycblt.auxio.home
import android.app.Application import android.app.Application
import android.content.SharedPreferences
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -39,9 +40,11 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class HomeViewModel(application: Application) : class HomeViewModel(application: Application) :
AndroidViewModel(application), Settings.Listener, MusicStore.Listener { AndroidViewModel(application),
MusicStore.Listener,
SharedPreferences.OnSharedPreferenceChangeListener {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val settings = Settings(application, this) private val settings = Settings(application)
private val _songsList = MutableStateFlow(listOf<Song>()) private val _songsList = MutableStateFlow(listOf<Song>())
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */ /** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
@ -91,13 +94,14 @@ class HomeViewModel(application: Application) :
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
init { init {
musicStore.addCallback(this) musicStore.addListener(this)
settings.addListener(this)
} }
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
musicStore.removeCallback(this) musicStore.removeListener(this)
settings.release() settings.removeListener(this)
} }
override fun onLibraryChanged(library: MusicStore.Library?) { override fun onLibraryChanged(library: MusicStore.Library?) {
@ -119,7 +123,7 @@ class HomeViewModel(application: Application) :
} }
} }
override fun onSettingChanged(key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) { when (key) {
context.getString(R.string.set_key_lib_tabs) -> { context.getString(R.string.set_key_lib_tabs) -> {
// Tabs changed, update the current tabs and set up a re-create event. // Tabs changed, update the current tabs and set up a re-create event.

View file

@ -28,7 +28,6 @@ import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -36,12 +35,8 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener { class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener {
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val tabAdapter = TabAdapter(this) private val tabAdapter = TabAdapter(this)
private val touchHelper: ItemTouchHelper by lifecycleObject { private var touchHelper: ItemTouchHelper? = null
ItemTouchHelper(TabDragCallback(tabAdapter))
}
override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater)
@ -50,13 +45,13 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
.setTitle(R.string.set_lib_tabs) .setTitle(R.string.set_lib_tabs)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
logD("Committing tab changes") logD("Committing tab changes")
settings.libTabs = tabAdapter.tabs Settings(requireContext()).libTabs = tabAdapter.tabs
} }
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
} }
override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) {
var tabs = settings.libTabs var tabs = Settings(requireContext()).libTabs
// Try to restore a pending tab configuration that was saved prior. // Try to restore a pending tab configuration that was saved prior.
if (savedInstanceState != null) { if (savedInstanceState != null) {
val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS)) val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS))
@ -69,7 +64,8 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
tabAdapter.submitTabs(tabs) tabAdapter.submitTabs(tabs)
binding.tabRecycler.apply { binding.tabRecycler.apply {
adapter = tabAdapter adapter = tabAdapter
touchHelper.attachToRecyclerView(this) touchHelper =
ItemTouchHelper(TabDragCallback(tabAdapter)).also { it.attachToRecyclerView(this) }
} }
} }
@ -105,7 +101,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
} }
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
touchHelper.startDrag(viewHolder) requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder)
} }
private companion object { private companion object {

View file

@ -35,7 +35,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Listener {
get() = _selected get() = _selected
init { init {
musicStore.addCallback(this) musicStore.addListener(this)
} }
override fun onLibraryChanged(library: MusicStore.Library?) { override fun onLibraryChanged(library: MusicStore.Library?) {
@ -58,7 +58,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Listener {
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
musicStore.removeCallback(this) musicStore.removeListener(this)
} }
/** /**

View file

@ -55,7 +55,7 @@ class MusicStore private constructor() {
* @see Listener * @see Listener
*/ */
@Synchronized @Synchronized
fun addCallback(listener: Listener) { fun addListener(listener: Listener) {
listener.onLibraryChanged(library) listener.onLibraryChanged(library)
listeners.add(listener) listeners.add(listener)
} }
@ -67,7 +67,7 @@ class MusicStore private constructor() {
* @see Listener * @see Listener
*/ */
@Synchronized @Synchronized
fun removeCallback(listener: Listener) { fun removeListener(listener: Listener) {
listeners.remove(listener) listeners.remove(listener)
} }

View file

@ -39,11 +39,11 @@ class MusicViewModel : ViewModel(), Indexer.Listener {
get() = _statistics get() = _statistics
init { init {
indexer.registerCallback(this) indexer.registerListener(this)
} }
override fun onCleared() { override fun onCleared() {
indexer.unregisterCallback(this) indexer.unregisterListener(this)
} }
override fun onIndexerStateChanged(state: Indexer.State?) { override fun onIndexerStateChanged(state: Indexer.State?) {

View file

@ -27,7 +27,6 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
/** /**
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to * A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
@ -35,8 +34,6 @@ import org.oxycblt.auxio.util.context
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() { class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
DialogSeparatorsBinding.inflate(inflater) DialogSeparatorsBinding.inflate(inflater)
@ -45,7 +42,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
.setTitle(R.string.set_separators) .setTitle(R.string.set_separators)
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
settings.musicSeparators = getCurrentSeparators() Settings(requireContext()).musicSeparators = getCurrentSeparators()
} }
} }
@ -61,7 +58,8 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
// More efficient to do one iteration through the separator list and initialize // More efficient to do one iteration through the separator list and initialize
// the corresponding CheckBox for each character instead of doing an iteration // the corresponding CheckBox for each character instead of doing an iteration
// through the separator list for each CheckBox. // through the separator list for each CheckBox.
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS) ?: settings.musicSeparators) (savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
?: Settings(requireContext()).musicSeparators)
?.forEach { ?.forEach {
when (it) { when (it) {
SEPARATOR_COMMA -> binding.separatorComma.isChecked = true SEPARATOR_COMMA -> binding.separatorComma.isChecked = true

View file

@ -47,7 +47,7 @@ class PickerViewModel : ViewModel(), MusicStore.Listener {
get() = _genreChoices get() = _genreChoices
override fun onCleared() { override fun onCleared() {
musicStore.removeCallback(this) musicStore.removeListener(this)
} }
override fun onLibraryChanged(library: MusicStore.Library?) { override fun onLibraryChanged(library: MusicStore.Library?) {

View file

@ -22,6 +22,7 @@ import android.os.Bundle
import android.os.storage.StorageManager import android.os.storage.StorageManager
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -42,10 +43,8 @@ import org.oxycblt.auxio.util.showToast
class MusicDirsDialog : class MusicDirsDialog :
ViewBindingDialogFragment<DialogMusicDirsBinding>(), DirectoryAdapter.Listener { ViewBindingDialogFragment<DialogMusicDirsBinding>(), DirectoryAdapter.Listener {
private val dirAdapter = DirectoryAdapter(this) private val dirAdapter = DirectoryAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null
private val storageManager: StorageManager by lifecycleObject { binding -> private var storageManager: StorageManager? = null
binding.context.getSystemServiceCompat(StorageManager::class)
}
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicDirsBinding.inflate(inflater) DialogMusicDirsBinding.inflate(inflater)
@ -57,7 +56,10 @@ class MusicDirsDialog :
.setNeutralButton(R.string.lbl_add, null) .setNeutralButton(R.string.lbl_add, null)
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
val dirs = settings.getMusicDirs(storageManager) val settings = Settings(requireContext())
val dirs =
settings.getMusicDirs(
requireNotNull(storageManager) { "StorageManager was not available" })
val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
if (dirs != newDirs) { if (dirs != newDirs) {
logD("Committing changes") logD("Committing changes")
@ -67,7 +69,11 @@ class MusicDirsDialog :
} }
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
val launcher = val context = requireContext()
val storageManager =
context.getSystemServiceCompat(StorageManager::class).also { storageManager = it }
openDocumentTreeLauncher =
registerForActivityResult( registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs) ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
@ -79,7 +85,10 @@ class MusicDirsDialog :
val dialog = it as AlertDialog val dialog = it as AlertDialog
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener {
logD("Opening launcher") logD("Opening launcher")
launcher.launch(null) requireNotNull(openDocumentTreeLauncher) {
"Document tree launcher was not available"
}
.launch(null)
} }
} }
@ -88,7 +97,7 @@ class MusicDirsDialog :
itemAnimator = null itemAnimator = null
} }
var dirs = settings.getMusicDirs(storageManager) var dirs = Settings(context).getMusicDirs(storageManager)
if (savedInstanceState != null) { if (savedInstanceState != null) {
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
@ -127,6 +136,8 @@ class MusicDirsDialog :
override fun onDestroyBinding(binding: DialogMusicDirsBinding) { override fun onDestroyBinding(binding: DialogMusicDirsBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
storageManager = null
openDocumentTreeLauncher = null
binding.dirsRecycler.adapter = null binding.dirsRecycler.adapter = null
} }
@ -153,7 +164,9 @@ class MusicDirsDialog :
DocumentsContract.buildDocumentUriUsingTree( DocumentsContract.buildDocumentUriUsingTree(
uri, DocumentsContract.getTreeDocumentId(uri)) uri, DocumentsContract.getTreeDocumentId(uri))
val treeUri = DocumentsContract.getTreeDocumentId(docUri) val treeUri = DocumentsContract.getTreeDocumentId(docUri)
val dir = Directory.fromDocumentTreeUri(storageManager, treeUri) val dir =
Directory.fromDocumentTreeUri(
requireNotNull(storageManager) { "StorageManager was not available" }, treeUri)
if (dir != null) { if (dir != null) {
dirAdapter.add(dir) dirAdapter.add(dir)

View file

@ -111,7 +111,7 @@ class Indexer private constructor() {
* @param listener The [Listener] to add. * @param listener The [Listener] to add.
*/ */
@Synchronized @Synchronized
fun registerCallback(listener: Listener) { fun registerListener(listener: Listener) {
if (BuildConfig.DEBUG && this.listener != null) { if (BuildConfig.DEBUG && this.listener != null) {
logW("Listener is already registered") logW("Listener is already registered")
return return
@ -131,7 +131,7 @@ class Indexer private constructor() {
* @see Listener * @see Listener
*/ */
@Synchronized @Synchronized
fun unregisterCallback(listener: Listener) { fun unregisterListener(listener: Listener) {
if (BuildConfig.DEBUG && this.listener !== listener) { if (BuildConfig.DEBUG && this.listener !== listener) {
logW("Given controller did not match current controller") logW("Given controller did not match current controller")
return return

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.music.system
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.database.ContentObserver import android.database.ContentObserver
import android.os.Handler import android.os.Handler
import android.os.IBinder import android.os.IBinder
@ -54,7 +55,8 @@ import org.oxycblt.auxio.util.logD
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class IndexerService : Service(), Indexer.Controller, Settings.Listener { class IndexerService :
Service(), Indexer.Controller, SharedPreferences.OnSharedPreferenceChangeListener {
private val indexer = Indexer.getInstance() private val indexer = Indexer.getInstance()
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
@ -81,7 +83,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Listener {
// Initialize any listener-dependent components last as we wouldn't want a listener race // Initialize any listener-dependent components last as we wouldn't want a listener race
// condition to cause us to load music before we were fully initialize. // condition to cause us to load music before we were fully initialize.
indexerContentObserver = SystemContentObserver() indexerContentObserver = SystemContentObserver()
settings = Settings(this, this) settings = Settings(this)
settings.addListener(this)
indexer.registerController(this) indexer.registerController(this)
// An indeterminate indexer and a missing library implies we are extremely early // An indeterminate indexer and a missing library implies we are extremely early
// in app initialization so start loading music. // in app initialization so start loading music.
@ -105,7 +108,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Listener {
// Then cancel the listener-dependent components to ensure that stray reloading // Then cancel the listener-dependent components to ensure that stray reloading
// events will not occur. // events will not occur.
indexerContentObserver.release() indexerContentObserver.release()
settings.release() settings.removeListener(this)
indexer.unregisterController(this) indexer.unregisterController(this)
// Then cancel any remaining music loading jobs. // Then cancel any remaining music loading jobs.
serviceJob.cancel() serviceJob.cancel()
@ -230,7 +233,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Listener {
// --- SETTING CALLBACKS --- // --- SETTING CALLBACKS ---
override fun onSettingChanged(key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) { when (key) {
// Hook changes in music settings to a new music loading event. // Hook changes in music settings to a new music loading event.
getString(R.string.set_key_exclude_non_music), getString(R.string.set_key_exclude_non_music),

View file

@ -23,6 +23,7 @@ import android.media.audiofx.AudioEffect
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@ -53,13 +54,7 @@ class PlaybackPanelFragment :
StyledSeekBar.Listener { StyledSeekBar.Listener {
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
// AudioEffect expects you to use startActivityForResult with the panel intent. There is no private var equalizerLauncher: ActivityResultLauncher<Intent>? = null
// contract analogue for this intent, so the generic contract is used instead.
private val equalizerLauncher by lifecycleObject {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
// Nothing to do
}
}
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
FragmentPlaybackPanelBinding.inflate(inflater) FragmentPlaybackPanelBinding.inflate(inflater)
@ -70,6 +65,13 @@ class PlaybackPanelFragment :
) { ) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
// AudioEffect expects you to use startActivityForResult with the panel intent. There is no
// contract analogue for this intent, so the generic contract is used instead.
equalizerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
// Nothing to do
}
// --- UI SETUP --- // --- UI SETUP ---
binding.root.setOnApplyWindowInsetsListener { view, insets -> binding.root.setOnApplyWindowInsetsListener { view, insets ->
val bars = insets.systemBarInsetsCompat val bars = insets.systemBarInsetsCompat
@ -116,6 +118,7 @@ class PlaybackPanelFragment :
} }
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) { override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
equalizerLauncher = null
binding.playbackToolbar.setOnMenuItemClickListener(null) binding.playbackToolbar.setOnMenuItemClickListener(null)
// Marquee elements leak if they are not disabled when the views are destroyed. // Marquee elements leak if they are not disabled when the views are destroyed.
binding.playbackSong.isSelected = false binding.playbackSong.isSelected = false
@ -137,7 +140,10 @@ class PlaybackPanelFragment :
// music playback. // music playback.
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
try { try {
equalizerLauncher.launch(equalizerIntent) requireNotNull(equalizerLauncher) {
"Equalizer panel launcher was not available"
}
.launch(equalizerIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
requireContext().showToast(R.string.err_no_app) requireContext().showToast(R.string.err_no_app)
} }

View file

@ -93,11 +93,11 @@ class PlaybackViewModel(application: Application) :
get() = playbackManager.currentAudioSessionId get() = playbackManager.currentAudioSessionId
init { init {
playbackManager.addCallback(this) playbackManager.addListener(this)
} }
override fun onCleared() { override fun onCleared() {
playbackManager.removeCallback(this) playbackManager.removeListener(this)
} }
override fun onIndexMoved(index: Int) { override fun onIndexMoved(index: Int) {

View file

@ -41,9 +41,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueAdapter.
private val queueModel: QueueViewModel by activityViewModels() private val queueModel: QueueViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val queueAdapter = QueueAdapter(this) private val queueAdapter = QueueAdapter(this)
private val touchHelper: ItemTouchHelper by lifecycleObject { private var touchHelper: ItemTouchHelper? = null
ItemTouchHelper(QueueDragCallback(queueModel))
}
override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater)
@ -53,7 +51,10 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueAdapter.
// --- UI SETUP --- // --- UI SETUP ---
binding.queueRecycler.apply { binding.queueRecycler.apply {
adapter = queueAdapter adapter = queueAdapter
touchHelper.attachToRecyclerView(this) touchHelper =
ItemTouchHelper(QueueDragCallback(queueModel)).also {
it.attachToRecyclerView(this)
}
} }
// Sometimes the scroll can change without the listener being updated, so we also // Sometimes the scroll can change without the listener being updated, so we also
@ -84,7 +85,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueAdapter.
} }
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
touchHelper.startDrag(viewHolder) requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder)
} }
private fun updateDivider() { private fun updateDivider() {

View file

@ -47,7 +47,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
var scrollTo: Int? = null var scrollTo: Int? = null
init { init {
playbackManager.addCallback(this) playbackManager.addListener(this)
} }
/** /**
@ -135,6 +135,6 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
playbackManager.removeCallback(this) playbackManager.removeListener(this)
} }
} }

View file

@ -26,15 +26,12 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
/** /**
* aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp]. * aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() { class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {
@ -42,7 +39,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
.setTitle(R.string.set_pre_amp) .setTitle(R.string.set_pre_amp)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
val binding = requireBinding() val binding = requireBinding()
settings.replayGainPreAmp = Settings(requireContext()).replayGainPreAmp =
ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value) ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value)
} }
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
@ -53,7 +50,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
// First initialization, we need to supply the sliders with the values from // First initialization, we need to supply the sliders with the values from
// settings. After this, the sliders save their own state, so we do not need to // settings. After this, the sliders save their own state, so we do not need to
// do any restore behavior. // do any restore behavior.
val preAmp = settings.replayGainPreAmp val preAmp = Settings(requireContext()).replayGainPreAmp
binding.withTagsSlider.value = preAmp.with binding.withTagsSlider.value = preAmp.with
binding.withoutTagsSlider.value = preAmp.without binding.withoutTagsSlider.value = preAmp.without
} }

View file

@ -99,7 +99,7 @@ class PlaybackStateManager private constructor() {
* @see Listener * @see Listener
*/ */
@Synchronized @Synchronized
fun addCallback(listener: Listener) { fun addListener(listener: Listener) {
if (isInitialized) { if (isInitialized) {
listener.onNewPlayback(index, queue, parent) listener.onNewPlayback(index, queue, parent)
listener.onRepeatChanged(repeatMode) listener.onRepeatChanged(repeatMode)
@ -117,7 +117,7 @@ class PlaybackStateManager private constructor() {
* @see Listener * @see Listener
*/ */
@Synchronized @Synchronized
fun removeCallback(listener: Listener) { fun removeListener(listener: Listener) {
listeners.remove(listener) listeners.remove(listener)
} }
@ -629,7 +629,7 @@ class PlaybackStateManager private constructor() {
/** /**
* The interface for receiving updates from [PlaybackStateManager]. Add the listener to * The interface for receiving updates from [PlaybackStateManager]. Add the listener to
* [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback]. * [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener].
*/ */
interface Listener { interface Listener {
/** /**

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.playback.system
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -47,7 +48,9 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class MediaSessionComponent(private val context: Context, private val listener: Listener) : class MediaSessionComponent(private val context: Context, private val listener: Listener) :
MediaSessionCompat.Callback(), PlaybackStateManager.Listener, Settings.Listener { MediaSessionCompat.Callback(),
PlaybackStateManager.Listener,
SharedPreferences.OnSharedPreferenceChangeListener {
private val mediaSession = private val mediaSession =
MediaSessionCompat(context, context.packageName).apply { MediaSessionCompat(context, context.packageName).apply {
isActive = true isActive = true
@ -55,13 +58,13 @@ class MediaSessionComponent(private val context: Context, private val listener:
} }
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context, this) private val settings = Settings(context)
private val notification = NotificationComponent(context, mediaSession.sessionToken) private val notification = NotificationComponent(context, mediaSession.sessionToken)
private val provider = BitmapProvider(context) private val provider = BitmapProvider(context)
init { init {
playbackManager.addCallback(this) playbackManager.addListener(this)
mediaSession.setCallback(this) mediaSession.setCallback(this)
} }
@ -79,15 +82,15 @@ class MediaSessionComponent(private val context: Context, private val listener:
*/ */
fun release() { fun release() {
provider.release() provider.release()
settings.release() settings.removeListener(this)
playbackManager.removeCallback(this) playbackManager.removeListener(this)
mediaSession.apply { mediaSession.apply {
isActive = false isActive = false
release() release()
} }
} }
// --- PLAYBACKSTATEMANAGER CALLBACKS --- // --- PLAYBACKSTATEMANAGER OVERRIDES ---
override fun onIndexMoved(index: Int) { override fun onIndexMoved(index: Int) {
updateMediaMetadata(playbackManager.song, playbackManager.parent) updateMediaMetadata(playbackManager.song, playbackManager.parent)
@ -139,9 +142,9 @@ class MediaSessionComponent(private val context: Context, private val listener:
invalidateSecondaryAction() invalidateSecondaryAction()
} }
// --- SETTINGSMANAGER CALLBACKS --- // --- SETTINGS OVERRIDES ---
override fun onSettingChanged(key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) { when (key) {
context.getString(R.string.set_key_cover_mode) -> context.getString(R.string.set_key_cover_mode) ->
updateMediaMetadata(playbackManager.song, playbackManager.parent) updateMediaMetadata(playbackManager.song, playbackManager.parent)
@ -149,7 +152,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
} }
} }
// --- MEDIASESSION CALLBACKS --- // --- MEDIASESSION OVERRIDES ---
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
super.onPlayFromMediaId(mediaId, extras) super.onPlayFromMediaId(mediaId, extras)

View file

@ -22,6 +22,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.SharedPreferences
import android.media.AudioManager import android.media.AudioManager
import android.media.audiofx.AudioEffect import android.media.audiofx.AudioEffect
import android.os.IBinder import android.os.IBinder
@ -80,8 +81,8 @@ class PlaybackService :
Player.Listener, Player.Listener,
InternalPlayer, InternalPlayer,
MediaSessionComponent.Listener, MediaSessionComponent.Listener,
Settings.Listener, MusicStore.Listener,
MusicStore.Listener { SharedPreferences.OnSharedPreferenceChangeListener {
// Player components // Player components
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var replayGainProcessor: ReplayGainAudioProcessor private lateinit var replayGainProcessor: ReplayGainAudioProcessor
@ -144,12 +145,13 @@ class PlaybackService :
.build() .build()
.also { it.addListener(this) } .also { it.addListener(this) }
// Initialize the core service components // Initialize the core service components
settings = Settings(this, this) settings = Settings(this)
settings.addListener(this)
foregroundManager = ForegroundManager(this) foregroundManager = ForegroundManager(this)
// Initialize any listener-dependent components last as we wouldn't want a listener race // Initialize any listener-dependent components last as we wouldn't want a listener race
// condition to cause us to load music before we were fully initialize. // condition to cause us to load music before we were fully initialize.
playbackManager.registerInternalPlayer(this) playbackManager.registerInternalPlayer(this)
musicStore.addCallback(this) musicStore.addListener(this)
widgetComponent = WidgetComponent(this) widgetComponent = WidgetComponent(this)
mediaSessionComponent = MediaSessionComponent(this, this) mediaSessionComponent = MediaSessionComponent(this, this)
registerReceiver( registerReceiver(
@ -185,12 +187,12 @@ class PlaybackService :
super.onDestroy() super.onDestroy()
foregroundManager.release() foregroundManager.release()
settings.release() settings.removeListener(this)
// Pause just in case this destruction was unexpected. // Pause just in case this destruction was unexpected.
playbackManager.setPlaying(false) playbackManager.setPlaying(false)
playbackManager.unregisterInternalPlayer(this) playbackManager.unregisterInternalPlayer(this)
musicStore.removeCallback(this) musicStore.removeListener(this)
unregisterReceiver(systemReceiver) unregisterReceiver(systemReceiver)
serviceJob.cancel() serviceJob.cancel()
@ -329,12 +331,13 @@ class PlaybackService :
} }
} }
// --- SETTINGSMANAGER OVERRIDES --- // --- SETTINGS OVERRIDES ---
override fun onSettingChanged(key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (key == getString(R.string.set_key_replay_gain) || if (key == getString(R.string.set_key_replay_gain) ||
key == getString(R.string.set_key_pre_amp_with) || key == getString(R.string.set_key_pre_amp_with) ||
key == getString(R.string.set_key_pre_amp_without)) { key == getString(R.string.set_key_pre_amp_without)) {
// ReplayGain changed, we need to set it up again.
onTracksChanged(player.currentTracks) onTracksChanged(player.currentTracks)
} }
} }

View file

@ -53,10 +53,8 @@ import org.oxycblt.auxio.util.*
class SearchFragment : ListFragment<FragmentSearchBinding>() { class SearchFragment : ListFragment<FragmentSearchBinding>() {
private val searchModel: SearchViewModel by androidViewModels() private val searchModel: SearchViewModel by androidViewModels()
private val searchAdapter = SearchAdapter(this) private val searchAdapter = SearchAdapter(this)
private var imm: InputMethodManager? = null
private var launchedKeyboard = false private var launchedKeyboard = false
private val imm: InputMethodManager by lifecycleObject { binding ->
binding.context.getSystemServiceCompat(InputMethodManager::class)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -74,13 +72,15 @@ class SearchFragment : ListFragment<FragmentSearchBinding>() {
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
imm = binding.context.getSystemServiceCompat(InputMethodManager::class)
binding.searchToolbar.apply { binding.searchToolbar.apply {
// Initialize the current filtering mode. // Initialize the current filtering mode.
menu.findItem(searchModel.getFilterOptionId()).isChecked = true menu.findItem(searchModel.getFilterOptionId()).isChecked = true
setNavigationOnClickListener { setNavigationOnClickListener {
// Keyboard is no longer needed. // Keyboard is no longer needed.
imm.hide() hideKeyboard()
findNavController().navigateUp() findNavController().navigateUp()
} }
@ -95,7 +95,7 @@ class SearchFragment : ListFragment<FragmentSearchBinding>() {
if (!launchedKeyboard) { if (!launchedKeyboard) {
// Auto-open the keyboard when this view is shown // Auto-open the keyboard when this view is shown
imm.show(this) showKeyboard(this)
launchedKeyboard = true launchedKeyboard = true
} }
} }
@ -184,7 +184,7 @@ class SearchFragment : ListFragment<FragmentSearchBinding>() {
else -> return else -> return
} }
// Keyboard is no longer needed. // Keyboard is no longer needed.
imm.hide() hideKeyboard()
findNavController().navigate(action) findNavController().navigate(action)
} }
@ -193,7 +193,7 @@ class SearchFragment : ListFragment<FragmentSearchBinding>() {
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) && if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
selected.isNotEmpty()) { selected.isNotEmpty()) {
// Make selection of obscured items easier by hiding the keyboard. // Make selection of obscured items easier by hiding the keyboard.
imm.hide() hideKeyboard()
} }
} }
@ -201,15 +201,19 @@ class SearchFragment : ListFragment<FragmentSearchBinding>() {
* Safely focus the keyboard on a particular [View]. * Safely focus the keyboard on a particular [View].
* @param view The [View] to focus the keyboard on. * @param view The [View] to focus the keyboard on.
*/ */
private fun InputMethodManager.show(view: View) { private fun showKeyboard(view: View) {
view.apply { view.apply {
requestFocus() requestFocus()
postDelayed(200) { showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) } postDelayed(200) {
requireNotNull(imm) { "InputMethodManager was not available" }
.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
} }
} }
/** Safely hide the keyboard from this view. */ /** Safely hide the keyboard from this view. */
private fun InputMethodManager.hide() { private fun hideKeyboard() {
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) requireNotNull(imm) { "InputMethodManager was not available" }
.hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
} }
} }

View file

@ -55,12 +55,12 @@ class SearchViewModel(application: Application) :
get() = _searchResults get() = _searchResults
init { init {
musicStore.addCallback(this) musicStore.addListener(this)
} }
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
musicStore.removeCallback(this) musicStore.removeListener(this)
} }
override fun onLibraryChanged(library: MusicStore.Library?) { override fun onLibraryChanged(library: MusicStore.Library?) {

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.settings
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build import android.os.Build
import android.os.storage.StorageManager import android.os.storage.StorageManager
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
@ -40,20 +41,14 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Object * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Member
* mutability * mutability is dependent on how they are used in app. Immutable members are often only modified by
* the preferences view, while mutable members are modified elsewhere.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class Settings(private val context: Context, private val listener: Listener? = null) : class Settings(private val context: Context) {
SharedPreferences.OnSharedPreferenceChangeListener {
private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
init {
if (listener != null) {
inner.registerOnSharedPreferenceChangeListener(this)
}
}
/** /**
* Migrate any settings from an old version into their modern counterparts. This can cause data * Migrate any settings from an old version into their modern counterparts. This can cause data
* loss depending on the feasibility of a migration. * loss depending on the feasibility of a migration.
@ -154,25 +149,19 @@ class Settings(private val context: Context, private val listener: Listener? = n
} }
/** /**
* Release this instance and any callbacks held by it. This is not needed if no [Listener] was * Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates.
* originally attached. * @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add.
*/ */
fun release() { fun addListener(listener: OnSharedPreferenceChangeListener) {
inner.unregisterOnSharedPreferenceChangeListener(this) inner.registerOnSharedPreferenceChangeListener(listener)
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
unlikelyToBeNull(listener).onSettingChanged(key)
}
/** Simplified listener for settings changes. */
interface Listener {
// TODO: Refactor this lifecycle
/** /**
* Called when a setting has changed. * Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further
* @param key The key of the setting that changed. * settings updates from being sent to ti.t
*/ */
fun onSettingChanged(key: String) fun removeListener(listener: OnSharedPreferenceChangeListener) {
inner.unregisterOnSharedPreferenceChangeListener(listener)
} }
// --- VALUES --- // --- VALUES ---

View file

@ -23,11 +23,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -37,7 +34,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
*/ */
abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() { abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
private var _binding: VB? = null private var _binding: VB? = null
private var lifecycleObjects = mutableListOf<LifecycleObject<VB, *>>()
/** /**
* Configure the [AlertDialog.Builder] during [onCreateDialog]. * Configure the [AlertDialog.Builder] during [onCreateDialog].
@ -85,25 +81,6 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
} }
} }
/**
* Delegate to automatically create and destroy an object derived from the [ViewBinding].
* @param create Block to create the object from the [ViewBinding].
*/
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
lifecycleObjects.add(LifecycleObject(null, create))
return object : ReadOnlyProperty<Fragment, T> {
private val objIdx = lifecycleObjects.lastIndex
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: Fragment, property: KProperty<*>) =
requireNotNull(lifecycleObjects[objIdx].data) {
"Cannot access lifecycle object when view does not exist"
}
as T
}
}
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -119,9 +96,6 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val binding = unlikelyToBeNull(_binding)
// Populate lifecycle-dependent objects
lifecycleObjects.forEach { it.populate(binding) }
// Configure binding // Configure binding
onBindingCreated(requireBinding(), savedInstanceState) onBindingCreated(requireBinding(), savedInstanceState)
// Apply the newly-configured view to the dialog. // Apply the newly-configured view to the dialog.
@ -132,21 +106,8 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
final override fun onDestroyView() { final override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
onDestroyBinding(unlikelyToBeNull(_binding)) onDestroyBinding(unlikelyToBeNull(_binding))
// Clear the lifecycle-dependent objects
lifecycleObjects.forEach { it.clear() }
// Clear binding // Clear binding
_binding = null _binding = null
logD("Fragment destroyed") logD("Fragment destroyed")
} }
/** Internal implementation of [lifecycleObject]. */
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
fun populate(binding: VB) {
data = create(binding)
}
fun clear() {
data = null
}
}
} }

View file

@ -23,8 +23,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -34,7 +32,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
*/ */
abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() { abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
private var _binding: VB? = null private var _binding: VB? = null
private var lifecycleObjects = mutableListOf<LifecycleObject<VB, *>>()
/** /**
* Inflate the [ViewBinding] during [onCreateView]. * Inflate the [ViewBinding] during [onCreateView].
@ -75,26 +72,6 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
} }
} }
/**
* Delegate to automatically create and destroy an object derived from the [ViewBinding].
* @param create Block to create the object from the [ViewBinding].
*/
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
// TODO: Phase this out.
lifecycleObjects.add(LifecycleObject(null, create))
return object : ReadOnlyProperty<Fragment, T> {
private val objIdx = lifecycleObjects.lastIndex
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: Fragment, property: KProperty<*>) =
requireNotNull(lifecycleObjects[objIdx].data) {
"Cannot access lifecycle object when view does not exist"
}
as T
}
}
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -103,9 +80,6 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val binding = unlikelyToBeNull(_binding)
// Populate lifecycle-dependent objects
lifecycleObjects.forEach { it.populate(binding) }
// Configure binding // Configure binding
onBindingCreated(requireBinding(), savedInstanceState) onBindingCreated(requireBinding(), savedInstanceState)
logD("Fragment created") logD("Fragment created")
@ -114,21 +88,8 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
final override fun onDestroyView() { final override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
onDestroyBinding(unlikelyToBeNull(_binding)) onDestroyBinding(unlikelyToBeNull(_binding))
// Clear the lifecycle-dependent objects
lifecycleObjects.forEach { it.clear() }
// Clear binding // Clear binding
_binding = null _binding = null
logD("Fragment destroyed") logD("Fragment destroyed")
} }
/** Internal implementation of [lifecycleObject]. */
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
fun populate(binding: VB) {
data = create(binding)
}
fun clear() {
data = null
}
}
} }

View file

@ -27,7 +27,6 @@ import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -38,7 +37,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class AccentCustomizeDialog : class AccentCustomizeDialog :
ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener { ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener {
private var accentAdapter = AccentAdapter(this) private var accentAdapter = AccentAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)
@ -46,6 +44,7 @@ class AccentCustomizeDialog :
builder builder
.setTitle(R.string.set_accent) .setTitle(R.string.set_accent)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
val settings = Settings(requireContext())
if (accentAdapter.selectedAccent == settings.accent) { if (accentAdapter.selectedAccent == settings.accent) {
// Nothing to do. // Nothing to do.
return@setPositiveButton return@setPositiveButton
@ -66,7 +65,7 @@ class AccentCustomizeDialog :
if (savedInstanceState != null) { if (savedInstanceState != null) {
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT)) Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
} else { } else {
settings.accent Settings(requireContext()).accent
}) })
} }

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.widgets package org.oxycblt.auxio.widgets
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import coil.request.ImageRequest import coil.request.ImageRequest
@ -41,14 +42,15 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class WidgetComponent(private val context: Context) : class WidgetComponent(private val context: Context) :
PlaybackStateManager.Listener, Settings.Listener { PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context, this) private val settings = Settings(context)
private val widgetProvider = WidgetProvider() private val widgetProvider = WidgetProvider()
private val provider = BitmapProvider(context) private val provider = BitmapProvider(context)
init { init {
playbackManager.addCallback(this) playbackManager.addListener(this)
settings.addListener(this)
} }
/** Update [WidgetProvider] with the current playback state. */ /** Update [WidgetProvider] with the current playback state. */
@ -104,9 +106,9 @@ class WidgetComponent(private val context: Context) :
/** Release this instance, preventing any further events from updating the widget instances. */ /** Release this instance, preventing any further events from updating the widget instances. */
fun release() { fun release() {
provider.release() provider.release()
settings.release() settings.removeListener(this)
widgetProvider.reset(context) widgetProvider.reset(context)
playbackManager.removeCallback(this) playbackManager.removeListener(this)
} }
// --- CALLBACKS --- // --- CALLBACKS ---
@ -118,7 +120,8 @@ class WidgetComponent(private val context: Context) :
override fun onStateChanged(state: InternalPlayer.State) = update() override fun onStateChanged(state: InternalPlayer.State) = update()
override fun onShuffledChanged(isShuffled: Boolean) = update() override fun onShuffledChanged(isShuffled: Boolean) = update()
override fun onRepeatChanged(repeatMode: RepeatMode) = update() override fun onRepeatChanged(repeatMode: RepeatMode) = update()
override fun onSettingChanged(key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (key == context.getString(R.string.set_key_cover_mode) || if (key == context.getString(R.string.set_key_cover_mode) ||
key == context.getString(R.string.set_key_round_mode)) { key == context.getString(R.string.set_key_round_mode)) {
update() update()

View file

@ -75,11 +75,12 @@ if ndk_path is None or not os.path.isfile(os.path.join(ndk_path, "ndk-build")):
candidates.append(entry.path) candidates.append(entry.path)
if len(candidates) > 0: if len(candidates) > 0:
print(WARN + "warn:" + NC + " NDK_PATH was not set or invalid. multiple " + print(WARN + "warn:" + NC + " ANDROID_NDK_HOME was not set or invalid. multiple " +
"candidates were found however:") "candidates were found however:")
for i, candidate in enumerate(candidates): for i, candidate in enumerate(candidates):
print("[" + str(i) + "] " + candidate) print("[" + str(i) + "] " + candidate)
print(WARN + "info:" + NC + " NDK r21e is recommended for this script. Other " +
"NDKs may result in unexpected behavior.")
try: try:
ndk_path = candidates[int(input("enter the ndk to use [default 0]: "))] ndk_path = candidates[int(input("enter the ndk to use [default 0]: "))]
except: except: