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.
This commit is contained in:
OxygenCobalt 2022-03-27 11:23:07 -06:00
parent 05a5ef5c3f
commit e54a58c612
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
29 changed files with 165 additions and 116 deletions

View file

@ -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) {

View file

@ -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

View file

@ -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) {

View file

@ -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
}

View file

@ -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<FragmentDetailBinding>() {
) {
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<FragmentDetailBinding>() {
onConfirm(sort.ascending(item.isChecked))
} else {
item.isChecked = true
onConfirm(requireNotNull(sort.assignId(item.itemId)))
onConfirm(unlikelyToBeNull(sort.assignId(item.itemId)))
}
true

View file

@ -56,7 +56,7 @@ class DetailViewModel : ViewModel() {
get() = settingsManager.detailAlbumSort
set(value) {
settingsManager.detailAlbumSort = value
refreshAlbumData()
currentAlbum.value?.let(::refreshAlbumData)
}
private val mCurrentArtist = MutableLiveData<Artist?>()
@ -70,7 +70,7 @@ class DetailViewModel : ViewModel() {
get() = settingsManager.detailArtistSort
set(value) {
settingsManager.detailArtistSort = value
refreshArtistData()
currentArtist.value?.let(::refreshArtistData)
}
private val mCurrentGenre = MutableLiveData<Genre?>()
@ -84,7 +84,7 @@ class DetailViewModel : ViewModel() {
get() = settingsManager.detailGenreSort
set(value) {
settingsManager.detailGenreSort = value
refreshGenreData()
currentGenre.value?.let(::refreshGenreData)
}
private val mNavToItem = MutableLiveData<Music?>()
@ -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<Item>(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<Item>(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<Item>(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
}
}

View file

@ -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

View file

@ -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) }

View file

@ -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<FragmentHomeBinding>() {
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<FragmentHomeBinding>() {
else -> {
item.isChecked = true
homeModel.updateCurrentSort(
requireNotNull(
unlikelyToBeNull(
homeModel
.getSortForDisplay(homeModel.currentTab.value!!)
.getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value))
.assignId(item.itemId)))
}
}

View file

@ -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 -> {}
}

View file

@ -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<Album>() {
}
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)) {

View file

@ -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<Artist>() {
}
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)

View file

@ -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<Genre>() {
}
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)

View file

@ -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<Song>() {
}
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

View file

@ -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

View file

@ -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<String, ExcludedAdapter.Listener>(binding.root) {
override fun bind(item: String, listener: ExcludedAdapter.Listener) {

View file

@ -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(

View file

@ -107,12 +107,10 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
}
}
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()
}

View file

@ -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) {

View file

@ -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)))
}
}

View file

@ -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)
}

View file

@ -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<Tab>
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))

View file

@ -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) }
}
}

View file

@ -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<CharSequence>
val values: IntArray
private var currentValue: Int? = null
@ -108,4 +105,9 @@ constructor(
return "<not set>"
}
}
companion object {
private val defValueField =
Preference::class.java.getDeclaredField("mDefaultValue").apply { isAccessible = true }
}
}

View file

@ -248,10 +248,13 @@ sealed class Sort(open val isAscending: Boolean) {
class NullableComparator<T : Comparable<T>> : Comparator<T?> {
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")
}
}
}

View file

@ -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)
}

View file

@ -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 <T> 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" }

View file

@ -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)
}

View file

@ -2,7 +2,7 @@
<resources>
<integer name="detail_app_bar_title_anim_duration">150</integer>
<!-- FIXME: This is really stupid, figure out how we can unify it with the IntegerTable object-->
<!-- FIXME: Unify this with the integer table object by simply defining a class for each dependency. Will reduce bugs. -->
<!-- Preference values -->
<string-array name="entries_theme">
<item>@string/set_theme_auto</item>