From e54a58c612b3934fc4d7a273e109403f40afc97e Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 27 Mar 2022 11:23:07 -0600 Subject: [PATCH] ui: audit null safety Audit null safety to remove extraneous and stupid calls while optimizing certain checks here and there. This commit is primarily centered around the introduction of a new utility: unlikelyToBeNull. This call uses requireNotNull on debug builds and !! on release builds, which allows for bug catching in an easier manner on normal builds while also allowing for optimizations on the release builds. --- .../auxio/accent/AccentCustomizeDialog.kt | 5 +- .../auxio/detail/AlbumDetailFragment.kt | 20 ++++---- .../auxio/detail/ArtistDetailFragment.kt | 7 +-- .../auxio/detail/DetailAppBarLayout.kt | 5 +- .../oxycblt/auxio/detail/DetailFragment.kt | 6 ++- .../oxycblt/auxio/detail/DetailViewModel.kt | 47 +++++++++--------- .../auxio/detail/GenreDetailFragment.kt | 9 ++-- .../detail/recycler/ArtistDetailAdapter.kt | 6 ++- .../org/oxycblt/auxio/home/HomeFragment.kt | 9 ++-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 9 ++-- .../auxio/home/list/AlbumListFragment.kt | 3 +- .../auxio/home/list/ArtistListFragment.kt | 7 ++- .../auxio/home/list/GenreListFragment.kt | 7 ++- .../auxio/home/list/SongListFragment.kt | 3 +- .../java/org/oxycblt/auxio/music/Music.kt | 10 ++-- ...udedEntryAdapter.kt => ExcludedAdapter.kt} | 5 +- .../auxio/music/excluded/ExcludedViewModel.kt | 10 ++-- .../auxio/playback/PlaybackBarFragment.kt | 2 - .../auxio/playback/PlaybackViewModel.kt | 6 ++- .../auxio/playback/system/AudioReactor.kt | 3 +- .../auxio/settings/SettingsListFragment.kt | 9 +++- .../oxycblt/auxio/settings/SettingsManager.kt | 3 +- .../auxio/settings/pref/IntListPrefDialog.kt | 48 ++++++++++--------- .../auxio/settings/pref/IntListPreference.kt | 8 ++-- .../main/java/org/oxycblt/auxio/ui/Sort.kt | 11 +++-- .../org/oxycblt/auxio/util/ContextUtil.kt | 2 +- .../org/oxycblt/auxio/util/PrimitiveUtil.kt | 16 +++++-- .../oxycblt/auxio/widgets/WidgetProvider.kt | 3 +- app/src/main/res/values/integers.xml | 2 +- 29 files changed, 165 insertions(+), 116 deletions(-) rename app/src/main/java/org/oxycblt/auxio/music/excluded/{ExcludedEntryAdapter.kt => ExcludedAdapter.kt} (94%) diff --git a/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt index 55f961667..a384b580c 100644 --- a/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt @@ -26,6 +26,7 @@ import org.oxycblt.auxio.databinding.DialogAccentBinding import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.unlikelyToBeNull /** * Dialog responsible for showing the list of accents to select. @@ -44,7 +45,7 @@ class AccentCustomizeDialog : builder.setPositiveButton(android.R.string.ok) { _, _ -> if (accentAdapter.selectedAccent != settingsManager.accent) { logD("Applying new accent") - settingsManager.accent = requireNotNull(accentAdapter.selectedAccent) + settingsManager.accent = unlikelyToBeNull(accentAdapter.selectedAccent) requireActivity().recreate() } @@ -71,7 +72,7 @@ class AccentCustomizeDialog : override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putInt(KEY_PENDING_ACCENT, requireNotNull(accentAdapter.selectedAccent).index) + outState.putInt(KEY_PENDING_ACCENT, unlikelyToBeNull(accentAdapter.selectedAccent).index) } override fun onDestroyBinding(binding: DialogAccentBinding) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index d48d70837..8d296135e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -41,6 +41,7 @@ import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.showToast +import org.oxycblt.auxio.util.unlikelyToBeNull /** * The [DetailFragment] for an album. @@ -53,15 +54,16 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { detailModel.setAlbumId(args.albumId) - setupToolbar(detailModel.currentAlbum.value!!, R.menu.menu_album_detail) { itemId -> + setupToolbar(unlikelyToBeNull(detailModel.currentAlbum.value), R.menu.menu_album_detail) { + itemId -> when (itemId) { R.id.action_play_next -> { - playbackModel.playNext(detailModel.currentAlbum.value!!) + playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value)) requireContext().showToast(R.string.lbl_queue_added) true } R.id.action_queue_add -> { - playbackModel.addToQueue(detailModel.currentAlbum.value!!) + playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value)) requireContext().showToast(R.string.lbl_queue_added) true } @@ -102,11 +104,11 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { } override fun onPlayParent() { - playbackModel.playAlbum(requireNotNull(detailModel.currentAlbum.value), false) + playbackModel.playAlbum(unlikelyToBeNull(detailModel.currentAlbum.value), false) } override fun onShuffleParent() { - playbackModel.playAlbum(requireNotNull(detailModel.currentAlbum.value), true) + playbackModel.playAlbum(unlikelyToBeNull(detailModel.currentAlbum.value), true) } override fun onShowSortMenu(anchor: View) { @@ -121,7 +123,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { findNavController() .navigate( AlbumDetailFragmentDirections.actionShowArtist( - requireNotNull(detailModel.currentAlbum.value).artist.id)) + unlikelyToBeNull(detailModel.currentAlbum.value).artist.id)) } private fun handleNavigation(item: Music?, adapter: AlbumDetailAdapter) { @@ -130,7 +132,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { // Songs should be scrolled to if the album matches, or a new detail // fragment should be launched otherwise. is Song -> { - if (detailModel.currentAlbum.value!!.id == item.album.id) { + if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.album.id) { logD("Navigating to a song in this album") scrollToItem(item.id, adapter) detailModel.finishNavToItem() @@ -144,7 +146,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { // If the album matches, no need to do anything. Otherwise launch a new // detail fragment. is Album -> { - if (detailModel.currentAlbum.value!!.id == item.id) { + if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.id) { logD("Navigating to the top of this album") binding.detailRecycler.scrollToPosition(0) detailModel.finishNavToItem() @@ -197,7 +199,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { } if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM && - playbackModel.parent.value?.id == detailModel.currentAlbum.value!!.id) { + playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) { adapter.highlightSong(song, binding.detailRecycler) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index b6ea6e0d5..b13aadb3a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -38,6 +38,7 @@ import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.unlikelyToBeNull /** * The [DetailFragment] for an artist. @@ -50,7 +51,7 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { detailModel.setArtistId(args.artistId) - setupToolbar(detailModel.currentArtist.value!!) + setupToolbar(unlikelyToBeNull(detailModel.currentArtist.value)) requireBinding().detailRecycler.apply { adapter = detailAdapter applySpans { pos -> @@ -91,11 +92,11 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { } override fun onPlayParent() { - playbackModel.playArtist(requireNotNull(detailModel.currentArtist.value), false) + playbackModel.playArtist(unlikelyToBeNull(detailModel.currentArtist.value), false) } override fun onShuffleParent() { - playbackModel.playArtist(requireNotNull(detailModel.currentArtist.value), true) + playbackModel.playArtist(unlikelyToBeNull(detailModel.currentArtist.value), true) } override fun onShowSortMenu(anchor: View) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt index b89548f56..aed147bf6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -101,8 +101,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr titleShown = visible - if (mTitleAnimator != null) { - mTitleAnimator!!.cancel() + val titleAnimator = mTitleAnimator + if (titleAnimator != null) { + titleAnimator.cancel() mTitleAnimator = null } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 7f14266d8..a6b044b55 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -32,6 +32,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A Base [Fragment] implementing the base features shared across all detail fragments. @@ -95,6 +96,9 @@ abstract class DetailFragment : ViewBindingFragment() { ) { logD("Launching menu") + // Scrolling breaks the menus, so we stop any momentum currently going on. + requireBinding().detailRecycler.stopScroll() + PopupMenu(anchor.context, anchor).apply { inflate(R.menu.menu_detail_sort) @@ -104,7 +108,7 @@ abstract class DetailFragment : ViewBindingFragment() { onConfirm(sort.ascending(item.isChecked)) } else { item.isChecked = true - onConfirm(requireNotNull(sort.assignId(item.itemId))) + onConfirm(unlikelyToBeNull(sort.assignId(item.itemId))) } true diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index bfd716126..af87d8ac6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -56,7 +56,7 @@ class DetailViewModel : ViewModel() { get() = settingsManager.detailAlbumSort set(value) { settingsManager.detailAlbumSort = value - refreshAlbumData() + currentAlbum.value?.let(::refreshAlbumData) } private val mCurrentArtist = MutableLiveData() @@ -70,7 +70,7 @@ class DetailViewModel : ViewModel() { get() = settingsManager.detailArtistSort set(value) { settingsManager.detailArtistSort = value - refreshArtistData() + currentArtist.value?.let(::refreshArtistData) } private val mCurrentGenre = MutableLiveData() @@ -84,7 +84,7 @@ class DetailViewModel : ViewModel() { get() = settingsManager.detailGenreSort set(value) { settingsManager.detailGenreSort = value - refreshGenreData() + currentGenre.value?.let(::refreshGenreData) } private val mNavToItem = MutableLiveData() @@ -99,22 +99,30 @@ class DetailViewModel : ViewModel() { fun setAlbumId(id: Long) { if (mCurrentAlbum.value?.id == id) return val musicStore = MusicStore.requireInstance() - mCurrentAlbum.value = musicStore.albums.find { it.id == id } - refreshAlbumData() + val album = + requireNotNull(musicStore.albums.find { it.id == id }) { "Invalid album ID provided " } + + mCurrentAlbum.value = album + refreshAlbumData(album) } fun setArtistId(id: Long) { if (mCurrentArtist.value?.id == id) return val musicStore = MusicStore.requireInstance() - mCurrentArtist.value = musicStore.artists.find { it.id == id } - refreshArtistData() + val artist = + requireNotNull(musicStore.artists.find { it.id == id }) { "Invalid artist ID provided" } + + mCurrentArtist.value = artist + refreshArtistData(artist) } fun setGenreId(id: Long) { if (mCurrentGenre.value?.id == id) return val musicStore = MusicStore.requireInstance() - mCurrentGenre.value = musicStore.genres.find { it.id == id } - refreshGenreData() + val genre = + requireNotNull(musicStore.genres.find { it.id == id }) { "Invalid genre ID provided" } + mCurrentGenre.value = genre + refreshGenreData(genre) } /** Navigate to an item, whether a song/album/artist */ @@ -132,38 +140,29 @@ class DetailViewModel : ViewModel() { isNavigating = navigating } - private fun refreshGenreData() { + private fun refreshGenreData(genre: Genre) { logD("Refreshing genre data") - val genre = requireNotNull(currentGenre.value) val data = mutableListOf(genre) - data.add(SortHeader(-2, R.string.lbl_songs)) - data.addAll(settingsManager.detailGenreSort.genre(currentGenre.value!!)) - + data.addAll(genreSort.genre(genre)) mGenreData.value = data } - private fun refreshArtistData() { + private fun refreshArtistData(artist: Artist) { logD("Refreshing artist data") - val artist = requireNotNull(currentArtist.value) val data = mutableListOf(artist) - data.add(Header(-2, R.string.lbl_albums)) data.addAll(Sort.ByYear(false).albums(artist.albums)) data.add(SortHeader(-3, R.string.lbl_songs)) - data.addAll(settingsManager.detailArtistSort.artist(artist)) - + data.addAll(artistSort.artist(artist)) mArtistData.value = data.toList() } - private fun refreshAlbumData() { + private fun refreshAlbumData(album: Album) { logD("Refreshing album data") - val album = requireNotNull(currentAlbum.value) val data = mutableListOf(album) - data.add(SortHeader(id = -2, R.string.lbl_albums)) - data.addAll(settingsManager.detailAlbumSort.album(currentAlbum.value!!)) - + data.addAll(albumSort.album(album)) mAlbumData.value = data } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index e744a02cd..f5bbae8c6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -37,6 +37,7 @@ import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.unlikelyToBeNull /** * The [DetailFragment] for a genre. @@ -49,7 +50,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { detailModel.setGenreId(args.genreId) - setupToolbar(detailModel.currentGenre.value!!) + setupToolbar(unlikelyToBeNull(detailModel.currentGenre.value)) binding.detailRecycler.apply { adapter = detailAdapter applySpans { pos -> @@ -83,11 +84,11 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { } override fun onPlayParent() { - playbackModel.playGenre(requireNotNull(detailModel.currentGenre.value), false) + playbackModel.playGenre(unlikelyToBeNull(detailModel.currentGenre.value), false) } override fun onShuffleParent() { - playbackModel.playGenre(requireNotNull(detailModel.currentGenre.value), true) + playbackModel.playGenre(unlikelyToBeNull(detailModel.currentGenre.value), true) } override fun onShowSortMenu(anchor: View) { @@ -119,7 +120,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { private fun updateSong(song: Song?, adapter: GenreDetailAdapter) { val binding = requireBinding() if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE && - playbackModel.parent.value?.id == detailModel.currentGenre.value!!.id) { + playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentGenre.value).id) { adapter.highlightSong(song, binding.detailRecycler) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt index 47d35c9c4..b8e036e43 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt @@ -179,7 +179,11 @@ private constructor( override fun bind(item: Album, listener: MenuItemListener) { binding.parentImage.bindAlbumCover(item) binding.parentName.textSafe = item.resolvedName - binding.parentInfo.textSafe = binding.context.getString(R.string.fmt_number, item.year) + binding.parentInfo.textSafe = if (item.year != null) { + binding.context.getString(R.string.fmt_number, item.year) + } else { + binding.context.getString(R.string.def_date) + } binding.root.apply { setOnClickListener { listener.onItemClick(item) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index b0782e67f..d66e74de9 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -49,6 +49,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logTraceOrThrow +import org.oxycblt.auxio.util.unlikelyToBeNull /** * The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each @@ -134,9 +135,9 @@ class HomeFragment : ViewBindingFragment() { R.id.option_sort_asc -> { item.isChecked = !item.isChecked homeModel.updateCurrentSort( - requireNotNull( + unlikelyToBeNull( homeModel - .getSortForDisplay(homeModel.currentTab.value!!) + .getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value)) .ascending(item.isChecked))) } @@ -144,9 +145,9 @@ class HomeFragment : ViewBindingFragment() { else -> { item.isChecked = true homeModel.updateCurrentSort( - requireNotNull( + unlikelyToBeNull( homeModel - .getSortForDisplay(homeModel.currentTab.value!!) + .getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value)) .assignId(item.itemId))) } } 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 edeeccfb4..a31b81d5a 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -32,6 +32,7 @@ import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.unlikelyToBeNull /** * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state. @@ -113,19 +114,19 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback { when (mCurrentTab.value) { DisplayMode.SHOW_SONGS -> { settingsManager.libSongSort = sort - mSongs.value = sort.songs(mSongs.value!!) + mSongs.value = sort.songs(unlikelyToBeNull(mSongs.value)) } DisplayMode.SHOW_ALBUMS -> { settingsManager.libAlbumSort = sort - mAlbums.value = sort.albums(mAlbums.value!!) + mAlbums.value = sort.albums(unlikelyToBeNull(mAlbums.value)) } DisplayMode.SHOW_ARTISTS -> { settingsManager.libArtistSort = sort - mArtists.value = sort.artists(mArtists.value!!) + mArtists.value = sort.artists(unlikelyToBeNull(mArtists.value)) } DisplayMode.SHOW_GENRES -> { settingsManager.libGenreSort = sort - mGenres.value = sort.genres(mGenres.value!!) + mGenres.value = sort.genres(unlikelyToBeNull(mGenres.value)) } else -> {} } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 41e0b3d93..0c59c0ef5 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -32,6 +32,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [HomeListFragment] for showing a list of [Album]s. @@ -50,7 +51,7 @@ class AlbumListFragment : HomeListFragment() { } override fun getPopup(pos: Int): String? { - val album = homeModel.albums.value!![pos] + val album = unlikelyToBeNull(homeModel.albums.value)[pos] // Change how we display the popup depending on the mode. return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 85ed5b280..7dba02641 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -30,6 +30,7 @@ import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.PrimitiveBackingData import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [HomeListFragment] for showing a list of [Artist]s. @@ -48,7 +49,11 @@ class ArtistListFragment : HomeListFragment() { } override fun getPopup(pos: Int) = - homeModel.artists.value!![pos].resolvedName.sliceArticle().first().uppercase() + unlikelyToBeNull(homeModel.artists.value)[pos] + .resolvedName + .sliceArticle() + .first() + .uppercase() override fun onItemClick(item: Item) { check(item is Artist) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index c7bc658dd..a8922a631 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -30,6 +30,7 @@ import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.PrimitiveBackingData import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [HomeListFragment] for showing a list of [Genre]s. @@ -48,7 +49,11 @@ class GenreListFragment : HomeListFragment() { } override fun getPopup(pos: Int) = - homeModel.genres.value!![pos].resolvedName.sliceArticle().first().uppercase() + unlikelyToBeNull(homeModel.genres.value)[pos] + .resolvedName + .sliceArticle() + .first() + .uppercase() override fun onItemClick(item: Item) { check(item is Genre) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 0552a4eab..d5c6da40f 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -30,6 +30,7 @@ import org.oxycblt.auxio.ui.SongViewHolder import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [HomeListFragment] for showing a list of [Song]s. @@ -48,7 +49,7 @@ class SongListFragment : HomeListFragment() { } override fun getPopup(pos: Int): String { - val song = homeModel.songs.value!![pos] + val song = unlikelyToBeNull(homeModel.songs.value)[pos] // Change how we display the popup depending on the mode. // We don't use the more correct resolve(Model)Name here, as sorts are largely 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 03064b22a..2ee85a563 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -21,6 +21,7 @@ import android.content.ContentUris import android.net.Uri import android.provider.MediaStore import org.oxycblt.auxio.ui.Item +import org.oxycblt.auxio.util.unlikelyToBeNull // --- MUSIC MODELS --- @@ -84,19 +85,16 @@ data class Song( /** The duration of this song, in seconds (rounded down) */ val seconds: Long get() = duration / 1000 - /** The seconds of this song, but as a duration. */ - val formattedDuration: String - get() = seconds.toDuration(false) private var mAlbum: Album? = null /** The album of this song. */ val album: Album - get() = requireNotNull(mAlbum) + get() = unlikelyToBeNull(mAlbum) private var mGenre: Genre? = null /** The genre of this song. Will be an "unknown genre" if the song does not have any. */ val genre: Genre - get() = requireNotNull(mGenre) + get() = unlikelyToBeNull(mGenre) /** An album name resolved to this song in particular. */ val resolvedAlbumName: String @@ -177,7 +175,7 @@ data class Album( private var mArtist: Artist? = null /** The parent artist of this album. */ val artist: Artist - get() = requireNotNull(mArtist) + get() = unlikelyToBeNull(mArtist) /** The artist name, resolved to this album in particular. */ val resolvedArtistName: String diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedAdapter.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedAdapter.kt index 75844d534..47cf4e190 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedAdapter.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.music.excluded import android.content.Context -import androidx.recyclerview.widget.DiffUtil import org.oxycblt.auxio.databinding.ItemExcludedDirBinding import org.oxycblt.auxio.ui.BindingViewHolder import org.oxycblt.auxio.ui.MonoAdapter @@ -40,9 +39,7 @@ class ExcludedAdapter(listener: Listener) : } } -/** - * The viewholder for [ExcludedAdapter]. Not intended for use in other adapters. - */ +/** The viewholder for [ExcludedAdapter]. Not intended for use in other adapters. */ class ExcludedViewHolder private constructor(private val binding: ItemExcludedDirBinding) : BindingViewHolder(binding.root) { override fun bind(item: String, listener: ExcludedAdapter.Listener) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt index 6db5683ef..2fc7e7d2b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.unlikelyToBeNull /** * ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal of @@ -52,8 +53,9 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo * called. */ fun addPath(path: String) { - if (!mPaths.value!!.contains(path)) { - mPaths.value!!.add(path) + val paths = unlikelyToBeNull(mPaths.value) + if (!paths.contains(path)) { + paths.add(path) mPaths.value = mPaths.value isModified = true } @@ -64,7 +66,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo * [save] is called. */ fun removePath(path: String) { - mPaths.value!!.remove(path) + unlikelyToBeNull(mPaths.value).remove(path) mPaths.value = mPaths.value isModified = true } @@ -73,7 +75,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo fun save(onDone: () -> Unit) { viewModelScope.launch(Dispatchers.IO) { val start = System.currentTimeMillis() - excludedDatabase.writePaths(mPaths.value!!) + excludedDatabase.writePaths(unlikelyToBeNull(mPaths.value)) isModified = false onDone() this@ExcludedViewModel.logD( 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 49b065806..39b196397 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -107,12 +107,10 @@ class PlaybackBarFragment : ViewBindingFragment() { } } - binding.playbackPlayPause.isActivated = playbackModel.isPlaying.value!! playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying -> binding.playbackPlayPause.isActivated = isPlaying } - binding.playbackProgressBar.progress = playbackModel.positionSeconds.value!!.toInt() playbackModel.positionSeconds.observe(viewLifecycleOwner) { position -> binding.playbackProgressBar.progress = position.toInt() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 3781621fc..4b29bc494 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -36,6 +36,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE +import org.oxycblt.auxio.util.unlikelyToBeNull /** * The ViewModel that provides a UI frontend for [PlaybackStateManager]. @@ -211,7 +212,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { * is called just before the change is committed so that the adapter can be updated. */ fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) { - val index = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size) + val index = + adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(mNextUp.value).size) if (index in playbackManager.queue.indices) { apply() playbackManager.removeQueueItem(index) @@ -222,7 +224,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { * is called just before the change is committed so that the adapter can be updated. */ fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean { - val delta = (playbackManager.queue.size - mNextUp.value!!.size) + val delta = (playbackManager.queue.size - unlikelyToBeNull(mNextUp.value).size) val from = adapterFrom + delta val to = adapterTo + delta if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt index 25694cfbb..e3c006df6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt @@ -34,6 +34,7 @@ import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.unlikelyToBeNull /** * Manages the current volume and playback state across ReplayGain and AudioFocus events. @@ -172,7 +173,7 @@ class AudioReactor(context: Context, private val callback: (Float) -> Unit) : } if (key in REPLAY_GAIN_TAGS) { - tags.add(GainTag(requireNotNull(key), parseReplayGainFloat(value))) + tags.add(GainTag(unlikelyToBeNull(key), parseReplayGainFloat(value))) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 5e2edb4ce..b68b5c030 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -34,8 +34,8 @@ import org.oxycblt.auxio.accent.AccentCustomizeDialog import org.oxycblt.auxio.home.tabs.TabCustomizeDialog import org.oxycblt.auxio.music.excluded.ExcludedDialog import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.settings.pref.IntListPrefDialog import org.oxycblt.auxio.settings.pref.IntListPreference +import org.oxycblt.auxio.settings.pref.IntListPreferenceDialog import org.oxycblt.auxio.util.hardRestart import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.logD @@ -74,9 +74,14 @@ class SettingsListFragment : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.prefs_main, rootKey) } + @Suppress("Deprecation") override fun onDisplayPreferenceDialog(preference: Preference) { if (preference is IntListPreference) { - IntListPrefDialog.from(preference).show(childFragmentManager, IntListPrefDialog.TAG) + // Creating our own preference dialog is hilariously difficult. For one, we need + // to override this random method within the class in order to + val dialog = IntListPreferenceDialog.from(preference) + dialog.setTargetFragment(this, 0) + dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) } else { super.onDisplayPreferenceDialog(preference) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt index 21038a85c..5e1e93310 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -28,6 +28,7 @@ import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.system.ReplayGainMode import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort +import org.oxycblt.auxio.util.unlikelyToBeNull /** * Wrapper around the [SharedPreferences] class that writes & reads values without a context. @@ -73,7 +74,7 @@ class SettingsManager private constructor(context: Context) : var libTabs: Array get() = Tab.fromSequence(prefs.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) - ?: Tab.fromSequence(Tab.SEQUENCE_DEFAULT)!! + ?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT)) set(value) { prefs.edit { putInt(KEY_LIB_TABS, Tab.toSequence(value)) diff --git a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt index 231fcc7ee..6114162ce 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt @@ -19,41 +19,43 @@ package org.oxycblt.auxio.settings.pref import android.app.Dialog import android.os.Bundle -import androidx.fragment.app.DialogFragment -import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceDialogFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.oxycblt.auxio.BuildConfig -/** The dialog shown whenever an [IntListPreference] is shown. */ -class IntListPrefDialog : DialogFragment() { +class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { + private val listPreference: IntListPreference + get() = (preference as IntListPreference) + private var pendingValueIndex = -1 + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = MaterialAlertDialogBuilder(requireActivity(), theme) - - // Since we have to store the preference key as an argument, we have to find the - // preference we need to use manually. - val pref = - requireNotNull( - (parentFragment as PreferenceFragmentCompat).preferenceManager.findPreference< - IntListPreference>(requireArguments().getString(ARG_KEY, null))) - - builder.setTitle(pref.title) - - builder.setSingleChoiceItems(pref.entries, pref.getValueIndex()) { _, index -> - pref.setValueIndex(index) + // PreferenceDialogFragmentCompat does not allow us to customize the actual creation + // of the alert dialog, so we have to manually override onCreateDialog and customize it + // ourselves. + val builder = MaterialAlertDialogBuilder(requireContext(), theme) + builder.setTitle(listPreference.title) + builder.setPositiveButton(null, null) + builder.setNegativeButton(android.R.string.cancel, null) + builder.setSingleChoiceItems(listPreference.entries, listPreference.getValueIndex()) { + _, + index -> + pendingValueIndex = index dismiss() } - - builder.setNegativeButton(android.R.string.cancel, null) - return builder.create() } + override fun onDialogClosed(positiveResult: Boolean) { + if (pendingValueIndex > -1) { + listPreference.setValueIndex(pendingValueIndex) + } + } + companion object { const val TAG = BuildConfig.APPLICATION_ID + ".tag.INT_PREF" - const val ARG_KEY = BuildConfig.APPLICATION_ID + ".arg.PREF_KEY" - fun from(pref: IntListPreference): IntListPrefDialog { - return IntListPrefDialog().apply { + fun from(pref: IntListPreference): IntListPreferenceDialog { + return IntListPreferenceDialog().apply { arguments = Bundle().apply { putString(ARG_KEY, pref.key) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt index 66d820968..8c1bc7b9e 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt @@ -33,9 +33,6 @@ constructor( defStyleRes: Int = 0 ) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { // Reflect into Preference to get the (normally inaccessible) default value. - private val defValueField = - Preference::class.java.getDeclaredField("mDefaultValue").apply { isAccessible = true } - val entries: Array val values: IntArray private var currentValue: Int? = null @@ -108,4 +105,9 @@ constructor( return "" } } + + companion object { + private val defValueField = + Preference::class.java.getDeclaredField("mDefaultValue").apply { isAccessible = true } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt index f4963cb03..6282c9412 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -248,10 +248,13 @@ sealed class Sort(open val isAscending: Boolean) { class NullableComparator> : Comparator { override fun compare(a: T?, b: T?): Int { - if (a == null && b != null) return -1 // -1 -> a < b - if (a == null && b == null) return 0 // 0 -> a = b - if (a != null && b == null) return 1 // 1 -> a > b - return a!!.compareTo(b!!) + return when { + a != null && b != null -> a.compareTo(b) + a == null && b != null -> -1 // a < b + a == null && b == null -> 0 // a = b + a != null && b == null -> 1 // a < b + else -> error("Unreachable") + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index 0f42730f8..ae1264c59 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -95,7 +95,7 @@ fun Context.getColorSafe(@ColorRes color: Int): Int { */ fun Context.getColorStateListSafe(@ColorRes color: Int): ColorStateList { return try { - requireNotNull(ContextCompat.getColorStateList(this, color)) + unlikelyToBeNull(ContextCompat.getColorStateList(this, color)) } catch (e: Exception) { handleResourceFailure(e, "color state list", getColorSafe(android.R.color.black).stateList) } diff --git a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt index ec72b3047..779280904 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt @@ -21,6 +21,7 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.os.Looper import androidx.fragment.app.Fragment +import org.oxycblt.auxio.BuildConfig /** * Shortcut for querying all items in a database and running [block] with the cursor returned. Will @@ -36,8 +37,17 @@ fun assertBackgroundThread() { } } -fun Fragment.requireAttached() { - if (isDetached) { - error("Fragment is detached from activity") +/** + * Sanitizes a nullable value that is not likely to be null. On debug builds, requireNotNull is + * used, while on release builds, the unsafe assertion operator [!!] ]is used + */ +fun unlikelyToBeNull(value: T?): T { + return if (BuildConfig.DEBUG) { + requireNotNull(value) + } else { + value!! } } + +/** Require the fragment is attached to an activity. */ +fun Fragment.requireAttached() = check(!isDetached) { "Fragment is detached from activity" } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 069445e62..da3a1a20c 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -42,6 +42,7 @@ import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.unlikelyToBeNull /** * Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively @@ -245,7 +246,7 @@ class WidgetProvider : AppWidgetProvider() { logW("No good widget layout found") val minimum = - requireNotNull(views.minByOrNull { it.key.width * it.key.height }?.value) + unlikelyToBeNull(views.minByOrNull { it.key.width * it.key.height }?.value) updateAppWidget(id, minimum) } diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index c8205bc9a..262953ae6 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -2,7 +2,7 @@ 150 - + @string/set_theme_auto