diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index 62730833e..3b7262ec0 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -75,7 +75,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - // Save any pending tab configurations to restore from when this dialog is re-created. + // Save any pending tab configurations to restore if this dialog is re-created. outState.putInt(KEY_TABS, Tab.toIntCode(tabAdapter.tabs)) } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt index fea140a44..a23ff1e14 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt @@ -44,16 +44,16 @@ abstract class PlayingIndicatorAdapter : RecyclerV override fun getItemCount() = currentList.size override fun onBindViewHolder(holder: VH, position: Int, payloads: List) { - if (payloads.isEmpty()) { - // Not updating any indicator-specific things, so delegate to the concrete - // adapter (actually bind the item) - onBindViewHolder(holder, position) - } - // Only try to update the playing indicator if the ViewHolder supports it if (holder is ViewHolder) { holder.updatePlayingIndicator(currentList[position] == currentItem, isPlaying) } + + if (payloads.isEmpty()) { + // Not updating any indicator-specific attributes, so delegate to the concrete + // adapter (actually bind the item) + onBindViewHolder(holder, position) + } } /** * Update the currently playing item in the list. diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 3b0751306..0151a4e66 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -14,7 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + +// We use a special naming convention for internal fields, disable the lints that check for that. @file:Suppress("PropertyName", "FunctionName") package org.oxycblt.auxio.music diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt index 58f9d34b2..f400184eb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt @@ -139,7 +139,7 @@ fun List.parseMultiValue(settings: Settings) = */ fun String.maybeParseSeparators(settings: Settings): List { // Get the separators the user desires. If null, there's nothing to do. - val separators = settings.separators ?: return listOf(this) + val separators = settings.musicSeparators ?: return listOf(this) return splitEscaped { separators.contains(it) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt index aeb04860e..2c8b37785 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt @@ -55,7 +55,7 @@ class SeparatorsDialog : ViewBindingDialogFragment() { if (binding.separatorSlash.isChecked) separators += SEPARATOR_SLASH if (binding.separatorPlus.isChecked) separators += SEPARATOR_PLUS if (binding.separatorAnd.isChecked) separators += SEPARATOR_AND - settings.separators = separators + settings.musicSeparators = separators } } @@ -71,7 +71,7 @@ class SeparatorsDialog : ViewBindingDialogFragment() { // More efficient to do one iteration through the separator list and initialize // the corresponding CheckBox for each character instead of doing an iteration // through the separator list for each CheckBox. - settings.separators?.forEach { + settings.musicSeparators?.forEach { when (it) { SEPARATOR_COMMA -> binding.separatorComma.isChecked = true SEPARATOR_SEMICOLON -> binding.separatorSemicolon.isChecked = true 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 0c06fd7f1..b9931919b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -85,7 +85,7 @@ class PlaybackBarFragment : ViewBindingFragment() { } private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) { - when (settings.actionMode) { + when (settings.playbackBarAction) { ActionMode.NEXT -> { binding.playbackSecondaryAction.apply { setIconResource(R.drawable.ic_skip_next_24) 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 e79e13b96..8243747c1 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 @@ -342,7 +342,7 @@ class MediaSessionComponent(private val context: Context, private val callback: // Android 13+ leverages custom actions in the notification. val extraAction = - when (settings.notifAction) { + when (settings.playbackNotificationAction) { ActionMode.SHUFFLE -> PlaybackStateCompat.CustomAction.Builder( PlaybackService.ACTION_INVERT_SHUFFLE, @@ -375,7 +375,7 @@ class MediaSessionComponent(private val context: Context, private val callback: private fun invalidateSecondaryAction() { invalidateSessionState() - when (settings.notifAction) { + when (settings.playbackNotificationAction) { ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.isShuffled) else -> notification.updateRepeatMode(playbackManager.repeatMode) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index 162505c58..d8a80a096 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -27,7 +27,6 @@ import androidx.core.net.toUri import androidx.core.view.updatePadding import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController -import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.transition.MaterialFadeThrough import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R @@ -41,7 +40,7 @@ import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat /** - * A [BottomSheetDialogFragment] that shows Auxio's about screen. + * A [ViewBindingFragment] that displays information about the app and the current music library. * @author Alexander Capehart (OxygenCobalt) */ class AboutFragment : ViewBindingFragment() { @@ -56,18 +55,21 @@ class AboutFragment : ViewBindingFragment() { override fun onCreateBinding(inflater: LayoutInflater) = FragmentAboutBinding.inflate(inflater) override fun onBindingCreated(binding: FragmentAboutBinding, savedInstanceState: Bundle?) { + super.onBindingCreated(binding, savedInstanceState) + + // --- UI SETUP --- + binding.aboutToolbar.setNavigationOnClickListener { findNavController().navigateUp() } binding.aboutContents.setOnApplyWindowInsetsListener { view, insets -> view.updatePadding(bottom = insets.systemBarInsetsCompat.bottom) insets } - binding.aboutToolbar.setNavigationOnClickListener { findNavController().navigateUp() } - binding.aboutVersion.text = BuildConfig.VERSION_NAME - binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) } + binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_SOURCE) } binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) } binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) } + // VIEWMODEL SETUP collectImmediately(musicModel.statistics, ::updateStatistics) } @@ -86,14 +88,15 @@ class AboutFragment : ViewBindingFragment() { (statistics?.durationMs ?: 0).formatDurationMs(false)) } - /** Go through the process of opening a [link] in a browser. */ - private fun openLinkInBrowser(link: String) { - logD("Opening $link") - + /** + * Open the given URI in a web browser. + * @param uri The URL to open. + */ + private fun openLinkInBrowser(uri: String) { + logD("Opening $uri") val context = requireContext() - val browserIntent = - Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + Intent(Intent.ACTION_VIEW, uri.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Android 11 seems to now handle the app chooser situations on its own now @@ -124,7 +127,7 @@ class AboutFragment : ViewBindingFragment() { browserIntent.setPackage(pkgName) startActivity(browserIntent) } catch (e: ActivityNotFoundException) { - // Not browser but an app chooser due to OEM garbage + // Not a browser but an app chooser browserIntent.setPackage(null) openAppChooser(browserIntent) } @@ -136,18 +139,24 @@ class AboutFragment : ViewBindingFragment() { } } + /** + * Open an app chooser for a given [Intent]. + * @param intent The [Intent] to show an app chooser for. + */ private fun openAppChooser(intent: Intent) { val chooserIntent = Intent(Intent.ACTION_CHOOSER) .putExtra(Intent.EXTRA_INTENT, intent) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(chooserIntent) } companion object { - private const val LINK_CODEBASE = "https://github.com/oxygencobalt/Auxio" - private const val LINK_FAQ = "$LINK_CODEBASE/blob/master/info/FAQ.md" - private const val LINK_LICENSES = "$LINK_CODEBASE/blob/master/info/LICENSES.md" + /** The URL to the source code. */ + private const val LINK_SOURCE = "https://github.com/oxygencobalt/Auxio" + /** The URL to the FAQ document. */ + private const val LINK_FAQ = "$LINK_SOURCE/blob/master/info/FAQ.md" + /** The URL to the licenses document. */ + private const val LINK_LICENSES = "$LINK_SOURCE/blob/master/info/LICENSES.md" } } 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 dcb82c655..a818ce3b5 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -40,12 +40,8 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull /** - * Auxio's settings. - * - * This object wraps [SharedPreferences] in a type-safe manner, allowing access to all of the major - * settings that Auxio uses. Mutability is determined by use, as some values are written by - * PreferenceManager and others are written by Auxio's code. - * + * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. + * Object mutability * @author Alexander Capehart (OxygenCobalt) */ class Settings(private val context: Context, private val callback: Callback? = null) : @@ -59,8 +55,8 @@ class Settings(private val context: Context, private val callback: Callback? = n } /** - * Try to migrate shared preference keys to their new versions. Only intended for use by - * AuxioApp. Compat code will persist for 6 months before being removed. + * Migrate any settings from an old version into their modern counterparts. This can cause + * data loss depending on the feasibility of a migration. */ fun migrate() { if (inner.contains(OldKeys.KEY_ACCENT3)) { @@ -117,7 +113,7 @@ class Settings(private val context: Context, private val callback: Callback? = n fun Int.migratePlaybackMode() = when (this) { - // Genre playback mode was retried in 3.0.0 + // Genre playback mode was removed in 3.0.0 IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS @@ -156,6 +152,10 @@ class Settings(private val context: Context, private val callback: Callback? = n } } + /** + * Release this instance and any callbacks held by it. This is not needed if no [Callback] + * was originally attached. + */ fun release() { inner.unregisterOnSharedPreferenceChangeListener(this) } @@ -164,25 +164,27 @@ class Settings(private val context: Context, private val callback: Callback? = n unlikelyToBeNull(callback).onSettingChanged(key) } - /** An interface for receiving some preference updates. */ + /** + * TODO: Remove this + */ interface Callback { fun onSettingChanged(key: String) } // --- VALUES --- - /** The current theme */ + /** The current theme. Represented by the [AppCompatDelegate] constants. */ val theme: Int get() = inner.getInt( context.getString(R.string.set_key_theme), AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - /** Whether the dark theme should be black or not */ + /** Whether to use a black background when a dark theme is currently used. */ val useBlackTheme: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_black_theme), false) - /** The current accent. */ + /** The current [Accent] (Color Scheme). */ var accent: Accent get() = Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT)) @@ -193,7 +195,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The current library tabs preferred by the user. */ + /** The tabs to show in the home UI. */ var libTabs: Array get() = Tab.fromIntCode( @@ -206,40 +208,40 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** Whether to hide collaborator artists or not. */ + /** Whether to hide artists considered "collaborators" from the home UI. */ val shouldHideCollaborators: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_hide_collaborators), false) - /** Whether to round additional UI elements (including album covers) */ + /** Whether to round additional UI elements that require album covers to be rounded. */ val roundMode: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_round_mode), false) - /** Which action to display on the playback bar. */ - val actionMode: ActionMode + /** The action to display on the playback bar. */ + val playbackBarAction: ActionMode get() = ActionMode.fromIntCode( inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE)) ?: ActionMode.NEXT - /** The custom action to display in the notification. */ - val notifAction: ActionMode + /** The action to display in the playback notification. */ + val playbackNotificationAction: ActionMode get() = ActionMode.fromIntCode( inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE)) ?: ActionMode.REPEAT - /** Whether to resume playback when a headset is connected (may not work well in all cases) */ + /** Whether to start playback when a headset is plugged in. */ val headsetAutoplay: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_headset_autoplay), false) - /** The current ReplayGain configuration */ + /** The current ReplayGain configuration. */ val replayGainMode: ReplayGainMode get() = ReplayGainMode.fromIntCode( inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) ?: ReplayGainMode.DYNAMIC - /** The current ReplayGain pre-amp configuration */ + /** The current ReplayGain pre-amp configuration. */ var replayGainPreAmp: ReplayGainPreAmp get() = ReplayGainPreAmp( @@ -253,7 +255,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** What queue to create when a song is selected from the library or search */ + /** What MusicParent item to play from when a Song is played from the home view. */ val libPlaybackMode: MusicMode get() = MusicMode.fromIntCode( @@ -262,8 +264,8 @@ class Settings(private val context: Context, private val callback: Callback? = n ?: MusicMode.SONGS /** - * What queue t create when a song is selected from an album/artist/genre. Null means to default - * to the currently shown item. + * What MusicParent item to play from when a Song is played from the detail view. + * Will be null if configured to play from the currently shown item. */ val detailPlaybackMode: MusicMode? get() = @@ -271,18 +273,15 @@ class Settings(private val context: Context, private val callback: Callback? = n inner.getInt( context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE)) - /** Whether shuffle should stay on when a new song is selected. */ + /** Whether to keep shuffle on when playing a new Song. */ val keepShuffle: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_keep_shuffle), true) - /** Whether to rewind when the back button is pressed. */ + /** Whether to rewind when the skip previous button is pressed before skipping back. */ val rewindWithPrev: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_rewind_prev), true) - /** - * Whether [org.oxycblt.auxio.playback.state.RepeatMode.TRACK] should pause when the track - * repeats - */ + /** Whether a song should pause after every repeat. */ val pauseOnRepeat: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false) @@ -290,28 +289,34 @@ class Settings(private val context: Context, private val callback: Callback? = n val shouldBeObserving: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_observing), false) - /** The strategy used when loading images. */ + /** The strategy used when loading album covers. */ val coverMode: CoverMode get() = CoverMode.fromIntCode( inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) ?: CoverMode.MEDIA_STORE - /** Whether to load all audio files, even ones not considered music. */ + /** Whether to exclude non-music audio files from the music library. */ val excludeNonMusic: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_exclude_non_music), true) - /** Get the list of directories that music should be hidden/loaded from. */ + /** + * Set the configuration on how to handle particular directories in the music library. + * @param storageManager [StorageManager] required to parse directories. + * @return The [MusicDirectories] configuration. + */ fun getMusicDirs(storageManager: StorageManager): MusicDirectories { val dirs = (inner.getStringSet(context.getString(R.string.set_key_music_dirs), null) ?: emptySet()) .mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) } - return MusicDirectories( dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false)) } - /** Set the list of directories that music should be hidden/loaded from. */ + /** + * Set the configuration on how to handle particular directories in the music library. + * @param musicDirs The new [MusicDirectories] configuration. + */ fun setMusicDirs(musicDirs: MusicDirectories) { inner.edit { putStringSet( @@ -323,8 +328,11 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The list of separators the user wants to parse by. */ - var separators: String? + /** + * A string of characters representing the desired separator characters to denote + * multi-value tags. + */ + var musicSeparators: String? // Differ from convention and store a string of separator characters instead of an int // code. This makes it easier to use in Regexes and makes it more extendable. get() = @@ -336,7 +344,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The current filter mode of the search tab */ + /** The type of Music the search view is currently filtering to. */ var searchFilterMode: MusicMode? get() = MusicMode.fromIntCode( @@ -350,7 +358,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The song sort mode on HomeFragment */ + /** The Song [Sort] mode used in the Home UI. */ var libSongSort: Sort get() = Sort.fromIntCode( @@ -363,7 +371,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The album sort mode on HomeFragment */ + /** The Album [Sort] mode used in the Home UI. */ var libAlbumSort: Sort get() = Sort.fromIntCode( @@ -376,7 +384,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The artist sort mode on HomeFragment */ + /** The Artist [Sort] mode used in the Home UI. */ var libArtistSort: Sort get() = Sort.fromIntCode( @@ -389,7 +397,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The genre sort mode on HomeFragment */ + /** The Genre [Sort] mode used in the Home UI. */ var libGenreSort: Sort get() = Sort.fromIntCode( @@ -402,7 +410,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The detail album sort mode */ + /** The [Sort] mode used in the Album Detail UI. */ var detailAlbumSort: Sort get() { var sort = @@ -425,7 +433,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The detail artist sort mode */ + /** The [Sort] mode used in the Artist Detail UI. */ var detailArtistSort: Sort get() = Sort.fromIntCode( @@ -438,7 +446,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The detail genre sort mode */ + /** The [Sort] mode used in the Genre Detail UI. */ var detailGenreSort: Sort get() = Sort.fromIntCode( @@ -451,7 +459,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** Cache of the old keys used in Auxio. */ + /** Legacy keys that are no longer used, but still have to be migrated. */ private object OldKeys { const val KEY_ACCENT3 = "auxio_accent" const val KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION" diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt index fb9f84299..c0574f646 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.databinding.FragmentSettingsBinding import org.oxycblt.auxio.shared.ViewBindingFragment /** - * A container [Fragment] for the settings menu. + * A [Fragment] wrapper containing the preference fragment and a companion Toolbar. * @author Alexander Capehart (OxygenCobalt) */ class SettingsFragment : ViewBindingFragment() { @@ -40,6 +40,7 @@ class SettingsFragment : ViewBindingFragment() { FragmentSettingsBinding.inflate(inflater) override fun onBindingCreated(binding: FragmentSettingsBinding, savedInstanceState: Bundle?) { + // Point AppBarLayout to the preference fragment's RecyclerView. binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view binding.settingsToolbar.setNavigationOnClickListener { findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/Accent.kt b/app/src/main/java/org/oxycblt/auxio/settings/accent/Accent.kt index 14b982f4a..019c4d537 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/Accent.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/accent/Accent.kt @@ -105,23 +105,27 @@ private val ACCENT_PRIMARY_COLORS = R.color.dynamic_primary) /** - * The data object for an accent. In the UI this is known as a "Color Scheme." This can be nominally - * used to gleam some attributes about a given color scheme, but this is not recommended. Attributes - * are the better option in nearly all cases. + * The data object for a colored theme to use in the UI. This can be nominally used to gleam some + * attributes about a given color scheme, but this is not recommended. Attributes are the better + * option in nearly all cases. * - * @property name The name of this accent - * @property theme The theme resource for this accent - * @property blackTheme The black theme resource for this accent - * @property primary The primary color resource for this accent + * @param index The unique number for this particular accent. * @author Alexander Capehart (OxygenCobalt) */ class Accent private constructor(val index: Int) : Item { + /** The name of this [Accent]. */ val name: Int get() = ACCENT_NAMES[index] + /** The theme resource for this accent. */ val theme: Int get() = ACCENT_THEMES[index] + /** + * The black theme resource for this accent. Identical to [theme], but with a black + * background. + */ val blackTheme: Int get() = ACCENT_BLACK_THEMES[index] + /** The accent's primary color. */ val primary: Int get() = ACCENT_PRIMARY_COLORS[index] @@ -130,31 +134,40 @@ class Accent private constructor(val index: Int) : Item { override fun hashCode() = index.hashCode() companion object { + /** + * Create a new instance. + * @param index The unique number for this particular accent. + * @return A new [Accent] with the specified [index]. If [index] is not within the + * range of valid accents, [index] will be [DEFAULT] instead. + */ fun from(index: Int): Accent { - if (index > (MAX - 1)) { - logW("Account outside of bounds [idx: $index, max: $MAX]") - return Accent(5) + if (index !in 0 until MAX ) { + logW("Accent is out of bounds [idx: $index]") + return Accent(DEFAULT) } - return Accent(index) } + /** + * The default accent. + */ val DEFAULT = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Use dynamic coloring on devices that support it. ACCENT_THEMES.lastIndex } else { + // Use blue everywhere else. 5 } /** - * The maximum amount of accents that are valid. This excludes the dynamic accent on - * versions that do not support it. + * The amount of valid accents. */ val MAX = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { ACCENT_THEMES.size } else { - // Disable the option for a dynamic accent + // Disable the option for a dynamic accent on unsupported devices. ACCENT_THEMES.size - 1 } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentAdapter.kt index a0fb2eae9..7d40fcebc 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentAdapter.kt @@ -29,11 +29,13 @@ import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.inflater /** - * An adapter that displays the accent palette. + * A [RecyclerView.Adapter] that displays [Accent] choices. + * @param listener A [BasicListListener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ class AccentAdapter(private val listener: BasicListListener) : RecyclerView.Adapter() { + /** The currently selected [Accent]. */ var selectedAccent: Accent? = null private set @@ -42,7 +44,7 @@ class AccentAdapter(private val listener: BasicListListener) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AccentViewHolder.new(parent) override fun onBindViewHolder(holder: AccentViewHolder, position: Int) = - throw IllegalStateException() + throw NotImplementedError() override fun onBindViewHolder( holder: AccentViewHolder, @@ -50,43 +52,63 @@ class AccentAdapter(private val listener: BasicListListener) : payloads: MutableList ) { val item = Accent.from(position) - if (payloads.isEmpty()) { + // Not a re-selection, re-bind with new data. holder.bind(item, listener) } - holder.setSelected(item == selectedAccent) } + /** + * Update the currently selected [Accent]. + * @param accent The new [Accent] to select. + */ fun setSelectedAccent(accent: Accent) { - if (accent == selectedAccent) return + if (accent == selectedAccent) { + // Nothing to do. + return + } + + // Update ViewHolders for the old selected accent and new selected accent. selectedAccent?.let { old -> notifyItemChanged(old.index, PAYLOAD_SELECTION_CHANGED) } selectedAccent = accent notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED) } companion object { - val PAYLOAD_SELECTION_CHANGED = Any() + private val PAYLOAD_SELECTION_CHANGED = Any() } } +/** + * A [RecyclerView.ViewHolder] that displays an [Accent] choice. Use [new] to create an instance. + * @author Alexander Capehart (OxygenCobalt) + */ class AccentViewHolder private constructor(private val binding: ItemAccentBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: Accent, listener: BasicListListener) { - setSelected(false) - + /** + * Bind new data to this instance. + * @param accent The new [Accent] to bind. + * @param listener A [BasicListListener] to bind interactions to. + */ + fun bind(accent: Accent, listener: BasicListListener) { binding.accent.apply { - backgroundTintList = context.getColorCompat(item.primary) - contentDescription = context.getString(item.name) + setOnClickListener { listener.onClick(accent) } + backgroundTintList = context.getColorCompat(accent.primary) + // Add a Tooltip based on the content description so that the purpose of this + // button can be clear. + contentDescription = context.getString(accent.name) TooltipCompat.setTooltipText(this, contentDescription) - setOnClickListener { listener.onClick(item) } } } + /** + * Set whether this [Accent] is selected or not. + * @param isSelected Whether this [Accent] is currently selected. + */ fun setSelected(isSelected: Boolean) { binding.accent.apply { - isEnabled = !isSelected iconTint = if (isSelected) { context.getAttrColorCompat(R.attr.colorSurface) @@ -97,6 +119,11 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin } companion object { + /** + * Create a new instance. + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ fun new(parent: View) = AccentViewHolder(ItemAccentBinding.inflate(parent.context.inflater)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentCustomizeDialog.kt index 1ce47d273..ba1ad5141 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentCustomizeDialog.kt @@ -32,7 +32,7 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull /** - * Dialog responsible for showing the list of accents to select. + * A [ViewBindingDialogFragment] that allows the user to configure the current [Accent]. * @author Alexander Capehart (OxygenCobalt) */ class AccentCustomizeDialog : ViewBindingDialogFragment(), BasicListListener { @@ -45,12 +45,14 @@ class AccentCustomizeDialog : ViewBindingDialogFragment(), builder .setTitle(R.string.set_accent) .setPositiveButton(R.string.lbl_ok) { _, _ -> - if (accentAdapter.selectedAccent != settings.accent) { - logD("Applying new accent") - settings.accent = unlikelyToBeNull(accentAdapter.selectedAccent) - requireActivity().recreate() + if (accentAdapter.selectedAccent == settings.accent) { + // Nothing to do. + return@setPositiveButton } + logD("Applying new accent") + settings.accent = unlikelyToBeNull(accentAdapter.selectedAccent) + requireActivity().recreate() dismiss() } .setNegativeButton(R.string.lbl_cancel, null) @@ -58,6 +60,7 @@ class AccentCustomizeDialog : ViewBindingDialogFragment(), override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) { binding.accentRecycler.adapter = accentAdapter + // Restore a previous pending accent if possible, otherwise select the current setting. accentAdapter.setSelectedAccent( if (savedInstanceState != null) { Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT)) @@ -68,6 +71,7 @@ class AccentCustomizeDialog : ViewBindingDialogFragment(), override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) + // Save any pending accent configuration to restore if this dialog is re-created. outState.putInt(KEY_PENDING_ACCENT, unlikelyToBeNull(accentAdapter.selectedAccent).index) } @@ -81,6 +85,6 @@ class AccentCustomizeDialog : ViewBindingDialogFragment(), } companion object { - const val KEY_PENDING_ACCENT = BuildConfig.APPLICATION_ID + ".key.PENDING_ACCENT" + private const val KEY_PENDING_ACCENT = BuildConfig.APPLICATION_ID + ".key.PENDING_ACCENT" } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt index 171e364e9..c27a9f583 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt @@ -26,9 +26,9 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getDimenPixels /** - * A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width - * of the RecyclerView. Adapted from this StackOverflow answer: - * https://stackoverflow.com/a/30256880/14143986 + * A [GridLayoutManager] that automatically sets the span size in order to use the most possible + * space in the [RecyclerView]. + * Derived from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986 */ class AccentGridLayoutManager( context: Context, @@ -39,7 +39,6 @@ class AccentGridLayoutManager( // We use 56dp here since that's the rough size of the accent item. // This will need to be modified if this is used beyond the accent dialog. private var columnWidth = context.getDimenPixels(R.dimen.size_accent_item) - private var lastWidth = -1 private var lastHeight = -1 @@ -48,10 +47,8 @@ class AccentGridLayoutManager( val totalSpace = width - paddingRight - paddingLeft spanCount = max(1, totalSpace / columnWidth) } - lastWidth = width lastHeight = height - super.onLayoutChildren(recycler, state) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt index 0cbe414e3..9b33a2738 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt @@ -32,10 +32,10 @@ import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.lazyReflectedField /** - * @brief An implementation of the built-in list preference, backed with integers. + * An implementation of a list-based preference backed with integers. * - * This is not implemented automatically, so a preference screen must override it's dialog opening - * code in order to handle the preference. + * The dialog this preference corresponds to is not handled automatically, so a preference screen + * must override onDisplayPreferenceDialog in order to handle it. * * @author Alexander Capehart (OxygenCobalt) */ @@ -47,39 +47,39 @@ constructor( defStyleAttr: Int = androidx.preference.R.attr.dialogPreferenceStyle, defStyleRes: Int = 0 ) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { + /** The names of each entry. */ val entries: Array + /** The corresponding integer values for each entry. */ val values: IntArray - private var offValue: Int? = -1 - private var icons: TypedArray? = null - private var currentValue: Int? = null - // Reflect into Preference to get the (normally inaccessible) default value. - private val defValue: Int - get() = PREFERENCE_DEFAULT_VALUE_FIELD.get(this) as Int + private var entryIcons: TypedArray? = null + private var offValue: Int? = -1 + private var currentValue: Int? = null init { val prefAttrs = context.obtainStyledAttributes( attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes) - // Can't piggy-back on ListPreference, we have to instead define our own - // attributes for entires/values. + // Can't depend on ListPreference due to it working with strings we have to instead + // define our own attributes for entries/values. entries = prefAttrs.getTextArrayOrThrow(R.styleable.IntListPreference_entries) values = context.resources.getIntArray( prefAttrs.getResourceIdOrThrow(R.styleable.IntListPreference_entryValues)) - // Additional values: offValue defines an "off" position + // entryIcons defines an additional set of icons to use for each entry. + val iconsId = prefAttrs.getResourceId(R.styleable.IntListPreference_entryIcons, -1) + if (iconsId > -1) { + entryIcons = context.resources.obtainTypedArray(iconsId) + } + + // offValue defines an value in which the preference should be disabled. val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1) if (offValueId > -1) { offValue = context.getInteger(offValueId) } - val iconsId = prefAttrs.getResourceId(R.styleable.IntListPreference_entryIcons, -1) - if (iconsId > -1) { - icons = context.resources.obtainTypedArray(iconsId) - } - prefAttrs.recycle() summaryProvider = IntListSummaryProvider() @@ -89,59 +89,73 @@ constructor( override fun onSetInitialValue(defaultValue: Any?) { super.onSetInitialValue(defaultValue) - if (defaultValue != null) { // If were given a default value, we need to assign it. setValue(defaultValue as Int) } else { - currentValue = getPersistedInt(defValue) + // Reflect into Preference to get the (normally inaccessible) default value. + currentValue = getPersistedInt(PREFERENCE_DEFAULT_VALUE_FIELD.get(this) as Int) } } - override fun shouldDisableDependents(): Boolean = currentValue == offValue + override fun shouldDisableDependents() = currentValue == offValue override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) val index = getValueIndex() if (index > -1) { - val resourceId = icons?.getResourceId(index, -1) ?: -1 + // If we have a specific icon to use, make sure it is set in the view. + val resourceId = entryIcons?.getResourceId(index, -1) ?: -1 if (resourceId > -1) { (holder.findViewById(android.R.id.icon) as ImageView).setImageResource(resourceId) } } } + /** + * Get the index of the current value. + * @return The index of the current value within [values], or -1 if the [IntListPreference] + * is not set. + */ fun getValueIndex(): Int { val curValue = currentValue - if (curValue != null) { return values.indexOf(curValue) } - return -1 } - /** Set a value using the index of it in [values] */ + /** + * Set the current value of this preference using it's index. + * @param index The index of the new value within [values]. Must be valid. + */ fun setValueIndex(index: Int) { setValue(values[index]) } private fun setValue(value: Int) { if (value != currentValue) { - currentValue = value + if (!callChangeListener(value)) { + // Listener rejected the value + return + } - callChangeListener(value) + // Update internal value. + currentValue = value notifyDependencyChange(shouldDisableDependents()) persistInt(value) notifyChanged() } } + /** + * Copy of ListPreference's [Preference.SummaryProvider] for this [IntListPreference]. + */ private inner class IntListSummaryProvider : SummaryProvider { override fun provideSummary(preference: IntListPreference): CharSequence { val index = getValueIndex() - if (index != -1) { + // Get the entry corresponding to the currently shown value. return entries[index] } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreferenceDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreferenceDialog.kt index dff2bd991..b66bfe309 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreferenceDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreferenceDialog.kt @@ -24,7 +24,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R /** - * @brief The companion dialog to [IntListPreference]. + * The companion dialog to [IntListPreference]. Use [new] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { @@ -34,7 +34,7 @@ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { override fun onCreateDialog(savedInstanceState: Bundle?) = // PreferenceDialogFragmentCompat does not allow us to customize the actual creation - // of the alert dialog, so we have to manually override onCreateDialog and customize it + // of the alert dialog, so we have to override onCreateDialog and create a new dialog // ourselves. MaterialAlertDialogBuilder(requireContext(), theme) .setTitle(listPreference.title) @@ -54,11 +54,18 @@ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { } companion object { + /** The tag to use when instantiating this dialog. */ const val TAG = BuildConfig.APPLICATION_ID + ".tag.INT_PREF" - fun from(pref: IntListPreference): IntListPreferenceDialog { + /** + * Create a new instance. + * @param preference The [IntListPreference] to display. + * @return A new instance. + */ + fun new(preference: IntListPreference): IntListPreferenceDialog { return IntListPreferenceDialog().apply { - arguments = Bundle().apply { putString(ARG_KEY, pref.key) } + // Populate the key field required by PreferenceDialogFragmentCompat. + arguments = Bundle().apply { putString(ARG_KEY, preference.key) } } } } 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 9e624e766..ec95eea08 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 @@ -19,7 +19,6 @@ package org.oxycblt.auxio.settings.prefs import android.os.Bundle import android.view.View -import androidx.annotation.DrawableRes import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.updatePadding import androidx.fragment.app.activityViewModels @@ -35,7 +34,6 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.SettingsFragmentDirections -import org.oxycblt.auxio.shared.NavigationViewModel import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.logD @@ -43,14 +41,12 @@ import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat /** - * The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat]. + * The [PreferenceFragmentCompat] that displays the list of settings. * @author Alexander Capehart (OxygenCobalt) */ -@Suppress("UNUSED") class PreferenceFragment : PreferenceFragmentCompat() { private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val musicModel: MusicViewModel by activityViewModels() - private val navModel: NavigationViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -58,10 +54,9 @@ class PreferenceFragment : PreferenceFragmentCompat() { preferenceManager.onDisplayPreferenceDialogListener = this preferenceScreen.children.forEach(::setupPreference) - // Make the RecycleView edge-to-edge capable + // Configure the RecyclerView to support edge-to-edge. view.findViewById(androidx.preference.R.id.recycler_view).apply { clipToPadding = false - setOnApplyWindowInsetsListener { _, insets -> updatePadding(bottom = insets.systemBarInsetsCompat.bottom) insets @@ -81,21 +76,22 @@ class PreferenceFragment : PreferenceFragmentCompat() { is IntListPreference -> { // Copy the built-in preference dialog launching code into our project so // we can automatically use the provided preference class. - val dialog = IntListPreferenceDialog.from(preference) + val dialog = IntListPreferenceDialog.new(preference) dialog.setTargetFragment(this, 0) dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) } is WrappedDialogPreference -> { - val context = requireContext() + // WrappedDialogPreference cannot launch a dialog on it's own, it has to + // be handled manually. val directions = when (preference.key) { - context.getString(R.string.set_key_accent) -> + getString(R.string.set_key_accent) -> SettingsFragmentDirections.goToAccentDialog() - context.getString(R.string.set_key_lib_tabs) -> + getString(R.string.set_key_lib_tabs) -> SettingsFragmentDirections.goToTabDialog() - context.getString(R.string.set_key_pre_amp) -> + getString(R.string.set_key_pre_amp) -> SettingsFragmentDirections.goToPreAmpDialog() - context.getString(R.string.set_key_music_dirs) -> + getString(R.string.set_key_music_dirs) -> SettingsFragmentDirections.goToMusicDirsDialog() getString(R.string.set_key_separators) -> SettingsFragmentDirections.goToSeparatorsDialog() @@ -110,6 +106,9 @@ class PreferenceFragment : PreferenceFragmentCompat() { override fun onPreferenceTreeClick(preference: Preference): Boolean { val context = requireContext() + // Hook generic preferences to their specified preferences + // TODO: These seem like good things to put into a side navigation view, if I choose to + // do one. when (preference.key) { context.getString(R.string.set_key_save_state) -> { playbackModel.savePlaybackState { saved -> @@ -125,6 +124,8 @@ class PreferenceFragment : PreferenceFragmentCompat() { context.getString(R.string.set_key_wipe_state) -> { playbackModel.wipePlaybackState { wiped -> if (wiped) { + // Use the nullable context, as we could try to show a toast when this + // fragment is no longer attached. this.context?.showToast(R.string.lbl_state_wiped) } else { this.context?.showToast(R.string.err_did_not_wipe) @@ -134,6 +135,8 @@ class PreferenceFragment : PreferenceFragmentCompat() { context.getString(R.string.set_key_restore_state) -> playbackModel.tryRestorePlaybackState { restored -> if (restored) { + // Use the nullable context, as we could try to show a toast when this + // fragment is no longer attached. this.context?.showToast(R.string.lbl_state_restored) } else { this.context?.showToast(R.string.err_did_not_restore) @@ -151,7 +154,10 @@ class PreferenceFragment : PreferenceFragmentCompat() { val context = requireActivity() val settings = Settings(context) - if (!preference.isVisible) return + if (!preference.isVisible) { + // Nothing to do. + return + } if (preference is PreferenceCategory) { preference.children.forEach(::setupPreference) @@ -188,15 +194,4 @@ class PreferenceFragment : PreferenceFragmentCompat() { } } } - - /** Convert an theme integer into an icon that can be used. */ - @DrawableRes - private fun Int.toThemeIcon(): Int { - return when (this) { - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.drawable.ic_auto_24 - AppCompatDelegate.MODE_NIGHT_NO -> R.drawable.ic_light_24 - AppCompatDelegate.MODE_NIGHT_YES -> R.drawable.ic_dark_24 - else -> R.drawable.ic_auto_24 - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/WrappedDialogPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/WrappedDialogPreference.kt index 77ccd7607..3b799222c 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/WrappedDialogPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/WrappedDialogPreference.kt @@ -22,8 +22,9 @@ import android.util.AttributeSet import androidx.preference.DialogPreference /** - * Wraps [DialogPreference] as to make it type-distinct from other preferences while also making it - * possible to use in a PreferenceScreen. + * Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that + * custom dialog preferences are handled. + * @author Alexander Capehart (OxygenCobalt) */ class WrappedDialogPreference @JvmOverloads