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