diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bbbf847e..d974ead12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,13 @@ #### What's Improved - Added ability to edit previously played or currently playing items in the queue +- Added support for date values formatted as "YYYYMMDD" #### What's Fixed - Fixed unreliable ReplayGain adjustment application in certain situations - Fixed crash that would occur in music folders dialog when user does not have a working file manager +- Fixed notification not updating due to settings changes #### What's Changed - Implemented new queue system diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt index e581a55a9..c36e9d838 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt @@ -28,30 +28,44 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * User configuration specific to the home UI. * @author Alexander Capehart (OxygenCobalt) */ -interface HomeSettings : Settings { +interface HomeSettings : Settings { /** The tabs to show in the home UI. */ var homeTabs: Array /** Whether to hide artists considered "collaborators" from the home UI. */ val shouldHideCollaborators: Boolean - private class Real(context: Context) : Settings.Real(context), HomeSettings { + interface Listener { + /** Called when the [homeTabs] configuration changes. */ + fun onTabsChanged() + /** Called when the [shouldHideCollaborators] configuration changes. */ + fun onHideCollaboratorsChanged() + } + + private class Real(context: Context) : Settings.Real(context), HomeSettings { override var homeTabs: Array get() = Tab.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT)) + getString(R.string.set_key_home_tabs), Tab.SEQUENCE_DEFAULT)) ?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT)) set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_lib_tabs), Tab.toIntCode(value)) + putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(value)) apply() } } override val shouldHideCollaborators: Boolean get() = - sharedPreferences.getBoolean( - context.getString(R.string.set_key_hide_collaborators), false) + sharedPreferences.getBoolean(getString(R.string.set_key_hide_collaborators), false) + + override fun onSettingChanged(key: String, listener: Listener) { + when (key) { + getString(R.string.set_key_home_tabs) -> listener.onTabsChanged() + getString(R.string.set_key_hide_collaborators) -> + listener.onHideCollaboratorsChanged() + } + } } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 0283fc579..2798f64f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -18,17 +18,14 @@ package org.oxycblt.auxio.home import android.app.Application -import android.content.SharedPreferences import androidx.lifecycle.AndroidViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.oxycblt.auxio.R import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Sort -import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD /** @@ -36,9 +33,7 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class HomeViewModel(application: Application) : - AndroidViewModel(application), - MusicStore.Listener, - SharedPreferences.OnSharedPreferenceChangeListener { + AndroidViewModel(application), MusicStore.Listener, HomeSettings.Listener { private val musicStore = MusicStore.getInstance() private val homeSettings = HomeSettings.from(application) private val musicSettings = MusicSettings.from(application) @@ -92,13 +87,13 @@ class HomeViewModel(application: Application) : init { musicStore.addListener(this) - homeSettings.addListener(this) + homeSettings.registerListener(this) } override fun onCleared() { super.onCleared() musicStore.removeListener(this) - homeSettings.removeListener(this) + homeSettings.unregisterListener(this) } override fun onLibraryChanged(library: Library?) { @@ -120,19 +115,16 @@ class HomeViewModel(application: Application) : } } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - when (key) { - context.getString(R.string.set_key_lib_tabs) -> { - // Tabs changed, update the current tabs and set up a re-create event. - currentTabModes = makeTabModes() - _shouldRecreate.value = true - } - context.getString(R.string.set_key_hide_collaborators) -> { - // Changes in the hide collaborator setting will change the artist contents - // of the library, consider it a library update. - onLibraryChanged(musicStore.library) - } - } + override fun onTabsChanged() { + // Tabs changed, update the current tabs and set up a re-create event. + currentTabModes = makeTabModes() + _shouldRecreate.value = true + } + + override fun onHideCollaboratorsChanged() { + // Changes in the hide collaborator setting will change the artist contents + // of the library, consider it a library update. + onLibraryChanged(musicStore.library) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt index cffa6df22..d5dd32dd1 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt @@ -27,16 +27,20 @@ import org.oxycblt.auxio.util.logD * User configuration specific to image loading. * @author Alexander Capehart (OxygenCobalt) */ -interface ImageSettings : Settings { +interface ImageSettings : Settings { /** The strategy to use when loading album covers. */ val coverMode: CoverMode - private class Real(context: Context) : Settings.Real(context), ImageSettings { + interface Listener { + /** Called when [coverMode] changes. */ + fun onCoverModeChanged() {} + } + + private class Real(context: Context) : Settings.Real(context), ImageSettings { override val coverMode: CoverMode get() = CoverMode.fromIntCode( - sharedPreferences.getInt( - context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) + sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) ?: CoverMode.MEDIA_STORE override fun migrate() { @@ -54,13 +58,19 @@ interface ImageSettings : Settings { } sharedPreferences.edit { - putInt(context.getString(R.string.set_key_cover_mode), mode.intCode) + putInt(getString(R.string.set_key_cover_mode), mode.intCode) remove(OLD_KEY_SHOW_COVERS) remove(OLD_KEY_QUALITY_COVERS) } } } + override fun onSettingChanged(key: String, listener: Listener) { + if (key == getString(R.string.set_key_cover_mode)) { + listOf(key, listener) + } + } + private companion object { const val OLD_KEY_SHOW_COVERS = "KEY_SHOW_COVERS" const val OLD_KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS" diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index 6de1e1a63..465f7c8df 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.util.getSystemServiceCompat * User configuration specific to music system. * @author Alexander Capehart (OxygenCobalt) */ -interface MusicSettings : Settings { +interface MusicSettings : Settings { /** The configuration on how to handle particular directories in the music library. */ var musicDirs: MusicDirectories /** Whether to exclude non-music audio files from the music library. */ @@ -54,50 +54,51 @@ interface MusicSettings : Settings { /** The [Sort] mode used in an [Genre]'s [Song] list. */ var genreSongSort: Sort - private class Real(context: Context) : Settings.Real(context), MusicSettings { + interface Listener { + /** Called when a setting controlling how music is loaded has changed. */ + fun onIndexingSettingChanged() {} + /** Called when the [shouldBeObserving] configuration has changed. */ + fun onObservingChanged() {} + } + + private class Real(context: Context) : Settings.Real(context), MusicSettings { private val storageManager = context.getSystemServiceCompat(StorageManager::class) override var musicDirs: MusicDirectories get() { val dirs = - (sharedPreferences.getStringSet( - context.getString(R.string.set_key_music_dirs), null) + (sharedPreferences.getStringSet(getString(R.string.set_key_music_dirs), null) ?: emptySet()) .mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) } return MusicDirectories( dirs, sharedPreferences.getBoolean( - context.getString(R.string.set_key_music_dirs_include), false)) + getString(R.string.set_key_music_dirs_include), false)) } set(value) { sharedPreferences.edit { putStringSet( - context.getString(R.string.set_key_music_dirs), + getString(R.string.set_key_music_dirs), value.dirs.map(Directory::toDocumentTreeUri).toSet()) - putBoolean( - context.getString(R.string.set_key_music_dirs_include), value.shouldInclude) + putBoolean(getString(R.string.set_key_music_dirs_include), value.shouldInclude) apply() } } override val excludeNonMusic: Boolean get() = - sharedPreferences.getBoolean( - context.getString(R.string.set_key_exclude_non_music), true) + sharedPreferences.getBoolean(getString(R.string.set_key_exclude_non_music), true) override val shouldBeObserving: Boolean - get() = - sharedPreferences.getBoolean(context.getString(R.string.set_key_observing), false) + get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false) override var multiValueSeparators: String // Differ from convention and store a string of separator characters instead of an int // code. This makes it easier to use and more extendable. - get() = - sharedPreferences.getString(context.getString(R.string.set_key_separators), "") - ?: "" + get() = sharedPreferences.getString(getString(R.string.set_key_separators), "") ?: "" set(value) { sharedPreferences.edit { - putString(context.getString(R.string.set_key_separators), value) + putString(getString(R.string.set_key_separators), value) apply() } } @@ -105,12 +106,11 @@ interface MusicSettings : Settings { override var songSort: Sort get() = Sort.fromIntCode( - sharedPreferences.getInt( - context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE)) + sharedPreferences.getInt(getString(R.string.set_key_songs_sort), Int.MIN_VALUE)) ?: Sort(Sort.Mode.ByName, true) set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode) + putInt(getString(R.string.set_key_songs_sort), value.intCode) apply() } } @@ -119,11 +119,11 @@ interface MusicSettings : Settings { get() = Sort.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE)) + getString(R.string.set_key_albums_sort), Int.MIN_VALUE)) ?: Sort(Sort.Mode.ByName, true) set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode) + putInt(getString(R.string.set_key_albums_sort), value.intCode) apply() } } @@ -132,11 +132,11 @@ interface MusicSettings : Settings { get() = Sort.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE)) + getString(R.string.set_key_artists_sort), Int.MIN_VALUE)) ?: Sort(Sort.Mode.ByName, true) set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode) + putInt(getString(R.string.set_key_artists_sort), value.intCode) apply() } } @@ -145,11 +145,11 @@ interface MusicSettings : Settings { get() = Sort.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE)) + getString(R.string.set_key_genres_sort), Int.MIN_VALUE)) ?: Sort(Sort.Mode.ByName, true) set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode) + putInt(getString(R.string.set_key_genres_sort), value.intCode) apply() } } @@ -159,7 +159,7 @@ interface MusicSettings : Settings { var sort = Sort.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE)) + getString(R.string.set_key_album_songs_sort), Int.MIN_VALUE)) ?: Sort(Sort.Mode.ByDisc, true) // Correct legacy album sort modes to Disc @@ -171,7 +171,7 @@ interface MusicSettings : Settings { } set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_detail_album_sort), value.intCode) + putInt(getString(R.string.set_key_album_songs_sort), value.intCode) apply() } } @@ -180,11 +180,11 @@ interface MusicSettings : Settings { get() = Sort.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE)) + getString(R.string.set_key_artist_songs_sort), Int.MIN_VALUE)) ?: Sort(Sort.Mode.ByDate, false) set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_detail_artist_sort), value.intCode) + putInt(getString(R.string.set_key_artist_songs_sort), value.intCode) apply() } } @@ -193,14 +193,24 @@ interface MusicSettings : Settings { get() = Sort.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE)) + getString(R.string.set_key_genre_songs_sort), Int.MIN_VALUE)) ?: Sort(Sort.Mode.ByName, true) set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_detail_genre_sort), value.intCode) + putInt(getString(R.string.set_key_genre_songs_sort), value.intCode) apply() } } + + override fun onSettingChanged(key: String, listener: Listener) { + when (key) { + getString(R.string.set_key_exclude_non_music), + getString(R.string.set_key_music_dirs), + getString(R.string.set_key_music_dirs_include), + getString(R.string.set_key_separators) -> listener.onIndexingSettingChanged() + getString(R.string.set_key_observing) -> listener.onObservingChanged() + } + } } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index 51bdd9465..6358cb9ce 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.music.system import android.app.Service import android.content.Intent -import android.content.SharedPreferences import android.database.ContentObserver import android.os.Handler import android.os.IBinder @@ -32,7 +31,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.oxycblt.auxio.BuildConfig -import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.storage.contentResolverSafe @@ -55,8 +53,7 @@ import org.oxycblt.auxio.util.logD * * @author Alexander Capehart (OxygenCobalt) */ -class IndexerService : - Service(), Indexer.Controller, SharedPreferences.OnSharedPreferenceChangeListener { +class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener { private val indexer = Indexer.getInstance() private val musicStore = MusicStore.getInstance() private val playbackManager = PlaybackStateManager.getInstance() @@ -84,7 +81,7 @@ class IndexerService : // condition to cause us to load music before we were fully initialize. indexerContentObserver = SystemContentObserver() settings = MusicSettings.from(this) - settings.addListener(this) + settings.registerListener(this) indexer.registerController(this) // An indeterminate indexer and a missing library implies we are extremely early // in app initialization so start loading music. @@ -108,7 +105,7 @@ class IndexerService : // Then cancel the listener-dependent components to ensure that stray reloading // events will not occur. indexerContentObserver.release() - settings.removeListener(this) + settings.unregisterListener(this) indexer.unregisterController(this) // Then cancel any remaining music loading jobs. serviceJob.cancel() @@ -230,22 +227,18 @@ class IndexerService : // --- SETTING CALLBACKS --- - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - when (key) { - // Hook changes in music settings to a new music loading event. - getString(R.string.set_key_exclude_non_music), - getString(R.string.set_key_music_dirs), - getString(R.string.set_key_music_dirs_include), - getString(R.string.set_key_separators) -> onStartIndexing(true) - getString(R.string.set_key_observing) -> { - // Make sure we don't override the service state with the observing - // notification if we were actively loading when the automatic rescanning - // setting changed. In such a case, the state will still be updated when - // the music loading process ends. - if (!indexer.isIndexing) { - updateIdleSession() - } - } + override fun onIndexingSettingChanged() { + // Music loading configuration changed, need to reload music. + onStartIndexing(true) + } + + override fun onObservingChanged() { + // Make sure we don't override the service state with the observing + // notification if we were actively loading when the automatic rescanning + // setting changed. In such a case, the state will still be updated when + // the music loading process ends. + if (!indexer.isIndexing) { + updateIdleSession() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index 1ca050570..040240308 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -65,7 +65,7 @@ class PlaybackBarFragment : ViewBindingFragment() { // Set up actions binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() } - setupSecondaryActions(binding, PlaybackSettings.from(context).playbackBarAction) + setupSecondaryActions(binding, PlaybackSettings.from(context).barAction) // Load the track color in manually as it's unclear whether the track actually supports // using a ColorStateList in the resources. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt index 8832de932..9e46e5946 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt @@ -31,11 +31,11 @@ import org.oxycblt.auxio.util.logD * User configuration specific to the playback system. * @author Alexander Capehart (OxygenCobalt) */ -interface PlaybackSettings : Settings { +interface PlaybackSettings : Settings { /** The action to display on the playback bar. */ - val playbackBarAction: ActionMode + val barAction: ActionMode /** The action to display in the playback notification. */ - val playbackNotificationAction: ActionMode + val notificationAction: ActionMode /** Whether to start playback when a headset is plugged in. */ val headsetAutoplay: Boolean /** The current ReplayGain configuration. */ @@ -59,75 +59,72 @@ interface PlaybackSettings : Settings { /** Whether a song should pause after every repeat. */ val pauseOnRepeat: Boolean - private class Real(context: Context) : Settings.Real(context), PlaybackSettings { + interface Listener { + /** Called when one of the ReplayGain configurations have changed. */ + fun onReplayGainSettingsChanged() {} + /** Called when [notificationAction] has changed. */ + fun onNotificationActionChanged() {} + } + + private class Real(context: Context) : Settings.Real(context), PlaybackSettings { override val inListPlaybackMode: MusicMode get() = MusicMode.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_library_song_playback_mode), - Int.MIN_VALUE)) + getString(R.string.set_key_in_list_playback_mode), Int.MIN_VALUE)) ?: MusicMode.SONGS override val inParentPlaybackMode: MusicMode? get() = MusicMode.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_detail_song_playback_mode), - Int.MIN_VALUE)) + getString(R.string.set_key_in_parent_playback_mode), Int.MIN_VALUE)) - override val playbackBarAction: ActionMode + override val barAction: ActionMode get() = ActionMode.fromIntCode( - sharedPreferences.getInt( - context.getString(R.string.set_key_bar_action), Int.MIN_VALUE)) + sharedPreferences.getInt(getString(R.string.set_key_bar_action), Int.MIN_VALUE)) ?: ActionMode.NEXT - override val playbackNotificationAction: ActionMode + override val notificationAction: ActionMode get() = ActionMode.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_notif_action), Int.MIN_VALUE)) + getString(R.string.set_key_notif_action), Int.MIN_VALUE)) ?: ActionMode.REPEAT override val headsetAutoplay: Boolean get() = - sharedPreferences.getBoolean( - context.getString(R.string.set_key_headset_autoplay), false) + sharedPreferences.getBoolean(getString(R.string.set_key_headset_autoplay), false) override val replayGainMode: ReplayGainMode get() = ReplayGainMode.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) + getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) ?: ReplayGainMode.DYNAMIC override var replayGainPreAmp: ReplayGainPreAmp get() = ReplayGainPreAmp( - sharedPreferences.getFloat( - context.getString(R.string.set_key_pre_amp_with), 0f), - sharedPreferences.getFloat( - context.getString(R.string.set_key_pre_amp_without), 0f)) + sharedPreferences.getFloat(getString(R.string.set_key_pre_amp_with), 0f), + sharedPreferences.getFloat(getString(R.string.set_key_pre_amp_without), 0f)) set(value) { sharedPreferences.edit { - putFloat(context.getString(R.string.set_key_pre_amp_with), value.with) - putFloat(context.getString(R.string.set_key_pre_amp_without), value.without) + putFloat(getString(R.string.set_key_pre_amp_with), value.with) + putFloat(getString(R.string.set_key_pre_amp_without), value.without) apply() } } override val keepShuffle: Boolean - get() = - sharedPreferences.getBoolean(context.getString(R.string.set_key_keep_shuffle), true) + get() = sharedPreferences.getBoolean(getString(R.string.set_key_keep_shuffle), true) override val rewindWithPrev: Boolean - get() = - sharedPreferences.getBoolean(context.getString(R.string.set_key_rewind_prev), true) + get() = sharedPreferences.getBoolean(getString(R.string.set_key_rewind_prev), true) override val pauseOnRepeat: Boolean - get() = - sharedPreferences.getBoolean( - context.getString(R.string.set_key_repeat_pause), false) + get() = sharedPreferences.getBoolean(getString(R.string.set_key_repeat_pause), false) override fun migrate() { // "Use alternate notification action" was converted to an ActionMode setting in 3.0.0. @@ -142,7 +139,7 @@ interface PlaybackSettings : Settings { } sharedPreferences.edit { - putInt(context.getString(R.string.set_key_notif_action), mode.intCode) + putInt(getString(R.string.set_key_notif_action), mode.intCode) remove(OLD_KEY_ALT_NOTIF_ACTION) apply() } @@ -170,9 +167,7 @@ interface PlaybackSettings : Settings { ?: MusicMode.SONGS sharedPreferences.edit { - putInt( - context.getString(R.string.set_key_library_song_playback_mode), - mode.intCode) + putInt(getString(R.string.set_key_in_list_playback_mode), mode.intCode) remove(OLD_KEY_LIB_PLAYBACK_MODE) apply() } @@ -188,7 +183,7 @@ interface PlaybackSettings : Settings { sharedPreferences.edit { putInt( - context.getString(R.string.set_key_detail_song_playback_mode), + getString(R.string.set_key_in_parent_playback_mode), mode?.intCode ?: Int.MIN_VALUE) remove(OLD_KEY_DETAIL_PLAYBACK_MODE) apply() @@ -196,6 +191,14 @@ interface PlaybackSettings : Settings { } } + override fun onSettingChanged(key: String, listener: Listener) { + 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_without)) { + listener.onReplayGainSettingsChanged() + } + } + companion object { const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION" const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2" diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index 8461eb6a1..3a2a39893 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.playback.replaygain import android.content.Context -import android.content.SharedPreferences import com.google.android.exoplayer2.C import com.google.android.exoplayer2.Format import com.google.android.exoplayer2.Player @@ -28,7 +27,6 @@ import com.google.android.exoplayer2.audio.BaseAudioProcessor import com.google.android.exoplayer2.util.MimeTypes import java.nio.ByteBuffer import kotlin.math.pow -import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.extractor.TextTags import org.oxycblt.auxio.playback.PlaybackSettings @@ -45,10 +43,10 @@ import org.oxycblt.auxio.util.logD * * @author Alexander Capehart (OxygenCobalt) */ -class ReplayGainAudioProcessor(private val context: Context) : - BaseAudioProcessor(), Player.Listener, SharedPreferences.OnSharedPreferenceChangeListener { +class ReplayGainAudioProcessor(context: Context) : + BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener { private val playbackManager = PlaybackStateManager.getInstance() - private val settings = PlaybackSettings.from(context) + private val playbackSettings = PlaybackSettings.from(context) private var lastFormat: Format? = null private var volume = 1f @@ -65,7 +63,7 @@ class ReplayGainAudioProcessor(private val context: Context) : */ fun addToListeners(player: Player) { player.addListener(this) - settings.addListener(this) + playbackSettings.registerListener(this) } /** @@ -75,7 +73,7 @@ class ReplayGainAudioProcessor(private val context: Context) : */ fun releaseFromListeners(player: Player) { player.removeListener(this) - settings.removeListener(this) + playbackSettings.unregisterListener(this) } // --- OVERRIDES --- @@ -98,13 +96,9 @@ class ReplayGainAudioProcessor(private val context: Context) : applyReplayGain(null) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { - if (key == context.getString(R.string.set_key_replay_gain) || - key == context.getString(R.string.set_key_pre_amp_with) || - key == context.getString(R.string.set_key_pre_amp_without)) { - // ReplayGain changed, we need to set it up again. - applyReplayGain(lastFormat) - } + override fun onReplayGainSettingsChanged() { + // ReplayGain config changed, we need to set it up again. + applyReplayGain(lastFormat) } // --- REPLAYGAIN PARSING --- @@ -116,14 +110,14 @@ class ReplayGainAudioProcessor(private val context: Context) : private fun applyReplayGain(format: Format?) { lastFormat = format val gain = parseReplayGain(format ?: return) - val preAmp = settings.replayGainPreAmp + val preAmp = playbackSettings.replayGainPreAmp val adjust = if (gain != null) { logD("Found ReplayGain adjustment $gain") // ReplayGain is configurable, so determine what to do based off of the mode. val useAlbumGain = - when (settings.replayGainMode) { + when (playbackSettings.replayGainMode) { // User wants track gain to be preferred. Default to album gain only if // there is no track gain. ReplayGainMode.TRACK -> gain.track == 0f diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 8a88c400e..d404b92f1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -116,7 +116,7 @@ class PlaybackStateManager private constructor() { */ @Synchronized fun registerInternalPlayer(internalPlayer: InternalPlayer) { - if (BuildConfig.DEBUG && this.internalPlayer != null) { + if (this.internalPlayer != null) { logW("Internal player is already registered") return } @@ -141,7 +141,7 @@ class PlaybackStateManager private constructor() { */ @Synchronized fun unregisterInternalPlayer(internalPlayer: InternalPlayer) { - if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) { + if (this.internalPlayer !== internalPlayer) { logW("Given internal player did not match current internal player") return } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index c00b943b6..bd6900c51 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.playback.system import android.content.Context import android.content.Intent -import android.content.SharedPreferences import android.graphics.Bitmap import android.net.Uri import android.os.Bundle @@ -31,6 +30,7 @@ import androidx.media.session.MediaButtonReceiver import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.image.BitmapProvider +import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.ActionMode @@ -51,7 +51,8 @@ import org.oxycblt.auxio.util.logD class MediaSessionComponent(private val context: Context, private val listener: Listener) : MediaSessionCompat.Callback(), PlaybackStateManager.Listener, - SharedPreferences.OnSharedPreferenceChangeListener { + ImageSettings.Listener, + PlaybackSettings.Listener { private val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true @@ -59,13 +60,14 @@ class MediaSessionComponent(private val context: Context, private val listener: } private val playbackManager = PlaybackStateManager.getInstance() - private val settings = PlaybackSettings.from(context) + private val playbackSettings = PlaybackSettings.from(context) private val notification = NotificationComponent(context, mediaSession.sessionToken) private val provider = BitmapProvider(context) init { playbackManager.addListener(this) + playbackSettings.registerListener(this) mediaSession.setCallback(this) } @@ -83,7 +85,7 @@ class MediaSessionComponent(private val context: Context, private val listener: */ fun release() { provider.release() - settings.removeListener(this) + playbackSettings.unregisterListener(this) playbackManager.removeListener(this) mediaSession.apply { isActive = false @@ -150,12 +152,14 @@ class MediaSessionComponent(private val context: Context, private val listener: // --- SETTINGS OVERRIDES --- - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - when (key) { - context.getString(R.string.set_key_cover_mode) -> - updateMediaMetadata(playbackManager.queue.currentSong, playbackManager.parent) - context.getString(R.string.set_key_notif_action) -> invalidateSecondaryAction() - } + override fun onCoverModeChanged() { + // Need to reload the metadata cover. + updateMediaMetadata(playbackManager.queue.currentSong, playbackManager.parent) + } + + override fun onNotificationActionChanged() { + // Need to re-load the action shown in the notification. + invalidateSecondaryAction() } // --- MEDIASESSION OVERRIDES --- @@ -359,7 +363,7 @@ class MediaSessionComponent(private val context: Context, private val listener: // Add the secondary action (either repeat/shuffle depending on the configuration) val secondaryAction = - when (settings.playbackNotificationAction) { + when (playbackSettings.notificationAction) { ActionMode.SHUFFLE -> PlaybackStateCompat.CustomAction.Builder( PlaybackService.ACTION_INVERT_SHUFFLE, @@ -393,7 +397,7 @@ class MediaSessionComponent(private val context: Context, private val listener: private fun invalidateSecondaryAction() { invalidateSessionState() - when (settings.playbackNotificationAction) { + when (playbackSettings.notificationAction) { ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.queue.isShuffled) else -> notification.updateRepeatMode(playbackManager.repeatMode) } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt index 05dac481a..881bc8940 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt @@ -27,21 +27,20 @@ import org.oxycblt.auxio.settings.Settings * User configuration specific to the search UI. * @author Alexander Capehart (OxygenCobalt) */ -interface SearchSettings : Settings { +interface SearchSettings : Settings { /** The type of Music the search view is currently filtering to. */ var searchFilterMode: MusicMode? - private class Real(context: Context) : Settings.Real(context), SearchSettings { + private class Real(context: Context) : Settings.Real(context), SearchSettings { override var searchFilterMode: MusicMode? get() = MusicMode.fromIntCode( sharedPreferences.getInt( - context.getString(R.string.set_key_search_filter), Int.MIN_VALUE)) + getString(R.string.set_key_search_filter), Int.MIN_VALUE)) set(value) { sharedPreferences.edit { putInt( - context.getString(R.string.set_key_search_filter), - value?.intCode ?: Int.MIN_VALUE) + getString(R.string.set_key_search_filter), value?.intCode ?: Int.MIN_VALUE) apply() } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index f9a82fca7..c1804d859 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -19,49 +19,75 @@ package org.oxycblt.auxio.settings import android.content.Context import android.content.SharedPreferences +import androidx.annotation.StringRes import androidx.preference.PreferenceManager +import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.unlikelyToBeNull /** * Abstract user configuration information. This interface has no functionality whatsoever. Concrete * implementations should be preferred instead. * @author Alexander Capehart (OxygenCobalt) */ -interface Settings { - /** Migrate any settings fields from older versions into their new counterparts. */ +interface Settings { + /** + * Migrate any settings fields from older versions into their new counterparts. + * @throws NotImplementedError If there is nothing to migrate. + */ fun migrate() { throw NotImplementedError() } /** - * Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates. - * @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add. + * Add a listener to monitor for settings updates. Will do nothing if + * @param listener The listener to add. */ - fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { - throw NotImplementedError() - } + fun registerListener(listener: L) /** - * Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further - * settings updates from being sent to ti.t + * Unregister a listener, preventing any further settings updates from being sent to it. + * @param listener The listener to unregister, must be the same as the current listener. */ - fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { - throw NotImplementedError() - } + fun unregisterListener(listener: L) /** * A framework-backed [Settings] implementation. * @param context [Context] required. */ - abstract class Real(protected val context: Context) : Settings { + abstract class Real(private val context: Context) : + Settings, SharedPreferences.OnSharedPreferenceChangeListener { protected val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) - override fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { - sharedPreferences.registerOnSharedPreferenceChangeListener(listener) + /** @see [Context.getString] */ + protected fun getString(@StringRes stringRes: Int) = context.getString(stringRes) + + private var listener: L? = null + + override fun registerListener(listener: L) { + if (this.listener == null) { + // Registering a listener when it was null prior, attach the callback. + sharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + this.listener = listener } - override fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) + override fun unregisterListener(listener: L) { + if (this.listener !== listener) { + logW("Given listener was not the current listener.") + } + this.listener = null + // No longer have a listener, detach from the preferences instance. + sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) } + + final override fun onSharedPreferenceChanged( + sharedPreferences: SharedPreferences, + key: String + ) { + onSettingChanged(key, unlikelyToBeNull(listener)) + } + + open fun onSettingChanged(key: String, listener: L) {} } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt index cb7bdc82c..b8daa3c7a 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt @@ -87,7 +87,7 @@ class PreferenceFragment : PreferenceFragmentCompat() { when (preference.key) { getString(R.string.set_key_accent) -> SettingsFragmentDirections.goToAccentDialog() - getString(R.string.set_key_lib_tabs) -> + getString(R.string.set_key_home_tabs) -> SettingsFragmentDirections.goToTabDialog() getString(R.string.set_key_pre_amp) -> SettingsFragmentDirections.goToPreAmpDialog() diff --git a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt index 0faf91fbc..dedc5efc4 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt @@ -26,7 +26,11 @@ import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.util.logD -interface UISettings : Settings { +/** + * User configuration for the general app UI. + * @author Alexander Capehart (OxygenCobalt) + */ +interface UISettings : Settings { /** The current theme. Represented by the AppCompatDelegate constants. */ val theme: Int /** Whether to use a black background when a dark theme is currently used. */ @@ -36,32 +40,33 @@ interface UISettings : Settings { /** Whether to round additional UI elements that require album covers to be rounded. */ val roundMode: Boolean - private class Real(context: Context) : Settings.Real(context), UISettings { + interface Listener { + /** Called when [roundMode] changes. */ + fun onRoundModeChanged() + } + + private class Real(context: Context) : Settings.Real(context), UISettings { override val theme: Int get() = sharedPreferences.getInt( - context.getString(R.string.set_key_theme), - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + getString(R.string.set_key_theme), AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) override val useBlackTheme: Boolean - get() = - sharedPreferences.getBoolean(context.getString(R.string.set_key_black_theme), false) + get() = sharedPreferences.getBoolean(getString(R.string.set_key_black_theme), false) override var accent: Accent get() = Accent.from( - sharedPreferences.getInt( - context.getString(R.string.set_key_accent), Accent.DEFAULT)) + sharedPreferences.getInt(getString(R.string.set_key_accent), Accent.DEFAULT)) set(value) { sharedPreferences.edit { - putInt(context.getString(R.string.set_key_accent), value.index) + putInt(getString(R.string.set_key_accent), value.index) apply() } } override val roundMode: Boolean - get() = - sharedPreferences.getBoolean(context.getString(R.string.set_key_round_mode), false) + get() = sharedPreferences.getBoolean(getString(R.string.set_key_round_mode), false) override fun migrate() { if (sharedPreferences.contains(OLD_KEY_ACCENT3)) { @@ -78,13 +83,19 @@ interface UISettings : Settings { } sharedPreferences.edit { - putInt(context.getString(R.string.set_key_accent), accent) + putInt(getString(R.string.set_key_accent), accent) remove(OLD_KEY_ACCENT3) apply() } } } + override fun onSettingChanged(key: String, listener: Listener) { + if (key == getString(R.string.set_key_round_mode)) { + listener.onRoundModeChanged() + } + } + companion object { const val OLD_KEY_ACCENT3 = "auxio_accent" } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index a4c62022d..180165d97 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -18,13 +18,13 @@ package org.oxycblt.auxio.widgets import android.content.Context -import android.content.SharedPreferences import android.graphics.Bitmap import android.os.Build import coil.request.ImageRequest import coil.transform.RoundedCornersTransformation import org.oxycblt.auxio.R import org.oxycblt.auxio.image.BitmapProvider +import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.image.extractor.SquareFrameTransform import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song @@ -43,15 +43,17 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class WidgetComponent(private val context: Context) : - PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener { + PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener { private val playbackManager = PlaybackStateManager.getInstance() - private val settings = UISettings.from(context) + private val uiSettings = UISettings.from(context) + private val imageSettings = ImageSettings.from(context) private val widgetProvider = WidgetProvider() private val provider = BitmapProvider(context) init { playbackManager.addListener(this) - settings.addListener(this) + uiSettings.registerListener(this) + imageSettings.registerListener(this) } /** Update [WidgetProvider] with the current playback state. */ @@ -76,7 +78,7 @@ class WidgetComponent(private val context: Context) : if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12, always round the cover with the widget's inner radius context.getDimenPixels(android.R.dimen.system_app_widget_inner_radius) - } else if (settings.roundMode) { + } else if (uiSettings.roundMode) { // < Android 12, but the user still enabled round mode. context.getDimenPixels(R.dimen.size_corners_medium) } else { @@ -107,27 +109,23 @@ class WidgetComponent(private val context: Context) : /** Release this instance, preventing any further events from updating the widget instances. */ fun release() { provider.release() - settings.removeListener(this) + uiSettings.unregisterListener(this) widgetProvider.reset(context) playbackManager.removeListener(this) } // --- CALLBACKS --- - // Hook all the major song-changing updates + the major player state updates - // to updating the "Now Playing" widget. + // Respond to all major song or player changes that will affect the widget override fun onIndexMoved(queue: Queue) = update() override fun onQueueReordered(queue: Queue) = update() override fun onNewPlayback(queue: Queue, parent: MusicParent?) = update() override fun onStateChanged(state: InternalPlayer.State) = update() override fun onRepeatChanged(repeatMode: RepeatMode) = update() - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - if (key == context.getString(R.string.set_key_cover_mode) || - key == context.getString(R.string.set_key_round_mode)) { - update() - } - } + // Respond to settings changes that will affect the widget + override fun onRoundModeChanged() = update() + override fun onCoverModeChanged() = update() /** * A condensed form of the playback state that is safe to use in AppWidgets. diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index b96505ed8..ff0dbbe2e 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -20,8 +20,8 @@ auxio_pre_amp_with auxio_pre_amp_without - auxio_library_playback_mode - auxio_detail_playback_mode + auxio_library_playback_mode + auxio_detail_playback_mode KEY_KEEP_SHUFFLE KEY_PREV_REWIND KEY_LOOP_PAUSE @@ -29,7 +29,7 @@ auxio_wipe_state auxio_restore_state - auxio_lib_tabs + auxio_lib_tabs auxio_hide_collaborators auxio_round_covers auxio_bar_action @@ -37,14 +37,14 @@ KEY_SEARCH_FILTER - auxio_songs_sort - auxio_albums_sort - auxio_artists_sort - auxio_genres_sort + auxio_songs_sort + auxio_albums_sort + auxio_artists_sort + auxio_genres_sort - auxio_album_sort - auxio_artist_sort - auxio_genre_sort + auxio_album_sort + auxio_artist_sort + auxio_genre_sort @string/set_theme_auto diff --git a/app/src/main/res/xml/prefs_main.xml b/app/src/main/res/xml/prefs_main.xml index 433581aa9..7e04cd41c 100644 --- a/app/src/main/res/xml/prefs_main.xml +++ b/app/src/main/res/xml/prefs_main.xml @@ -27,7 +27,7 @@ @@ -86,7 +86,7 @@ app:defaultValue="@integer/music_mode_songs" app:entries="@array/entries_library_song_playback_mode" app:entryValues="@array/values_library_song_playback_mode" - app:key="@string/set_key_library_song_playback_mode" + app:key="@string/set_key_in_list_playback_mode" app:title="@string/set_library_song_playback_mode" app:useSimpleSummaryProvider="true" /> @@ -94,7 +94,7 @@ app:defaultValue="@integer/music_mode_none" app:entries="@array/entries_detail_song_playback_mode" app:entryValues="@array/values_detail_song_playback_mode" - app:key="@string/set_key_detail_song_playback_mode" + app:key="@string/set_key_in_parent_playback_mode" app:title="@string/set_detail_song_playback_mode" app:useSimpleSummaryProvider="true" /> diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt index 795e7b29c..a0a3096a6 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt @@ -20,6 +20,8 @@ package org.oxycblt.auxio.music import org.oxycblt.auxio.music.storage.MusicDirectories interface FakeMusicSettings : MusicSettings { + override fun registerListener(listener: MusicSettings.Listener) = throw NotImplementedError() + override fun unregisterListener(listener: MusicSettings.Listener) = throw NotImplementedError() override var musicDirs: MusicDirectories get() = throw NotImplementedError() set(_) = throw NotImplementedError()