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:
parent
bf56a50b59
commit
493b0a9f32
30 changed files with 175 additions and 232 deletions
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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 ---
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue