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.settings.SettingsManager
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Dialog responsible for showing the list of accents to select. * Dialog responsible for showing the list of accents to select.
@ -44,7 +45,7 @@ class AccentCustomizeDialog :
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
if (accentAdapter.selectedAccent != settingsManager.accent) { if (accentAdapter.selectedAccent != settingsManager.accent) {
logD("Applying new accent") logD("Applying new accent")
settingsManager.accent = requireNotNull(accentAdapter.selectedAccent) settingsManager.accent = unlikelyToBeNull(accentAdapter.selectedAccent)
requireActivity().recreate() requireActivity().recreate()
} }
@ -71,7 +72,7 @@ class AccentCustomizeDialog :
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) 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) { 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.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* The [DetailFragment] for an album. * The [DetailFragment] for an album.
@ -53,15 +54,16 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setAlbumId(args.albumId) 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) { when (itemId) {
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(detailModel.currentAlbum.value!!) playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lbl_queue_added) requireContext().showToast(R.string.lbl_queue_added)
true true
} }
R.id.action_queue_add -> { R.id.action_queue_add -> {
playbackModel.addToQueue(detailModel.currentAlbum.value!!) playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lbl_queue_added) requireContext().showToast(R.string.lbl_queue_added)
true true
} }
@ -102,11 +104,11 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
} }
override fun onPlayParent() { override fun onPlayParent() {
playbackModel.playAlbum(requireNotNull(detailModel.currentAlbum.value), false) playbackModel.playAlbum(unlikelyToBeNull(detailModel.currentAlbum.value), false)
} }
override fun onShuffleParent() { override fun onShuffleParent() {
playbackModel.playAlbum(requireNotNull(detailModel.currentAlbum.value), true) playbackModel.playAlbum(unlikelyToBeNull(detailModel.currentAlbum.value), true)
} }
override fun onShowSortMenu(anchor: View) { override fun onShowSortMenu(anchor: View) {
@ -121,7 +123,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
findNavController() findNavController()
.navigate( .navigate(
AlbumDetailFragmentDirections.actionShowArtist( AlbumDetailFragmentDirections.actionShowArtist(
requireNotNull(detailModel.currentAlbum.value).artist.id)) unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
} }
private fun handleNavigation(item: Music?, adapter: AlbumDetailAdapter) { 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 // Songs should be scrolled to if the album matches, or a new detail
// fragment should be launched otherwise. // fragment should be launched otherwise.
is Song -> { 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") logD("Navigating to a song in this album")
scrollToItem(item.id, adapter) scrollToItem(item.id, adapter)
detailModel.finishNavToItem() detailModel.finishNavToItem()
@ -144,7 +146,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
// If the album matches, no need to do anything. Otherwise launch a new // If the album matches, no need to do anything. Otherwise launch a new
// detail fragment. // detail fragment.
is Album -> { 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") logD("Navigating to the top of this album")
binding.detailRecycler.scrollToPosition(0) binding.detailRecycler.scrollToPosition(0)
detailModel.finishNavToItem() detailModel.finishNavToItem()
@ -197,7 +199,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
} }
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM && 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) adapter.highlightSong(song, binding.detailRecycler)
} else { } else {
// Clear the ViewHolders if the mode isn't ALL_SONGS // 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.applySpans
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* The [DetailFragment] for an artist. * The [DetailFragment] for an artist.
@ -50,7 +51,7 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setArtistId(args.artistId) detailModel.setArtistId(args.artistId)
setupToolbar(detailModel.currentArtist.value!!) setupToolbar(unlikelyToBeNull(detailModel.currentArtist.value))
requireBinding().detailRecycler.apply { requireBinding().detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
applySpans { pos -> applySpans { pos ->
@ -91,11 +92,11 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
} }
override fun onPlayParent() { override fun onPlayParent() {
playbackModel.playArtist(requireNotNull(detailModel.currentArtist.value), false) playbackModel.playArtist(unlikelyToBeNull(detailModel.currentArtist.value), false)
} }
override fun onShuffleParent() { override fun onShuffleParent() {
playbackModel.playArtist(requireNotNull(detailModel.currentArtist.value), true) playbackModel.playArtist(unlikelyToBeNull(detailModel.currentArtist.value), true)
} }
override fun onShowSortMenu(anchor: View) { override fun onShowSortMenu(anchor: View) {

View file

@ -101,8 +101,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
titleShown = visible titleShown = visible
if (mTitleAnimator != null) { val titleAnimator = mTitleAnimator
mTitleAnimator!!.cancel() if (titleAnimator != null) {
titleAnimator.cancel()
mTitleAnimator = null 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.Sort
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A Base [Fragment] implementing the base features shared across all detail fragments. * A Base [Fragment] implementing the base features shared across all detail fragments.
@ -95,6 +96,9 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
) { ) {
logD("Launching menu") logD("Launching menu")
// Scrolling breaks the menus, so we stop any momentum currently going on.
requireBinding().detailRecycler.stopScroll()
PopupMenu(anchor.context, anchor).apply { PopupMenu(anchor.context, anchor).apply {
inflate(R.menu.menu_detail_sort) inflate(R.menu.menu_detail_sort)
@ -104,7 +108,7 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
onConfirm(sort.ascending(item.isChecked)) onConfirm(sort.ascending(item.isChecked))
} else { } else {
item.isChecked = true item.isChecked = true
onConfirm(requireNotNull(sort.assignId(item.itemId))) onConfirm(unlikelyToBeNull(sort.assignId(item.itemId)))
} }
true true

View file

@ -56,7 +56,7 @@ class DetailViewModel : ViewModel() {
get() = settingsManager.detailAlbumSort get() = settingsManager.detailAlbumSort
set(value) { set(value) {
settingsManager.detailAlbumSort = value settingsManager.detailAlbumSort = value
refreshAlbumData() currentAlbum.value?.let(::refreshAlbumData)
} }
private val mCurrentArtist = MutableLiveData<Artist?>() private val mCurrentArtist = MutableLiveData<Artist?>()
@ -70,7 +70,7 @@ class DetailViewModel : ViewModel() {
get() = settingsManager.detailArtistSort get() = settingsManager.detailArtistSort
set(value) { set(value) {
settingsManager.detailArtistSort = value settingsManager.detailArtistSort = value
refreshArtistData() currentArtist.value?.let(::refreshArtistData)
} }
private val mCurrentGenre = MutableLiveData<Genre?>() private val mCurrentGenre = MutableLiveData<Genre?>()
@ -84,7 +84,7 @@ class DetailViewModel : ViewModel() {
get() = settingsManager.detailGenreSort get() = settingsManager.detailGenreSort
set(value) { set(value) {
settingsManager.detailGenreSort = value settingsManager.detailGenreSort = value
refreshGenreData() currentGenre.value?.let(::refreshGenreData)
} }
private val mNavToItem = MutableLiveData<Music?>() private val mNavToItem = MutableLiveData<Music?>()
@ -99,22 +99,30 @@ class DetailViewModel : ViewModel() {
fun setAlbumId(id: Long) { fun setAlbumId(id: Long) {
if (mCurrentAlbum.value?.id == id) return if (mCurrentAlbum.value?.id == id) return
val musicStore = MusicStore.requireInstance() val musicStore = MusicStore.requireInstance()
mCurrentAlbum.value = musicStore.albums.find { it.id == id } val album =
refreshAlbumData() requireNotNull(musicStore.albums.find { it.id == id }) { "Invalid album ID provided " }
mCurrentAlbum.value = album
refreshAlbumData(album)
} }
fun setArtistId(id: Long) { fun setArtistId(id: Long) {
if (mCurrentArtist.value?.id == id) return if (mCurrentArtist.value?.id == id) return
val musicStore = MusicStore.requireInstance() val musicStore = MusicStore.requireInstance()
mCurrentArtist.value = musicStore.artists.find { it.id == id } val artist =
refreshArtistData() requireNotNull(musicStore.artists.find { it.id == id }) { "Invalid artist ID provided" }
mCurrentArtist.value = artist
refreshArtistData(artist)
} }
fun setGenreId(id: Long) { fun setGenreId(id: Long) {
if (mCurrentGenre.value?.id == id) return if (mCurrentGenre.value?.id == id) return
val musicStore = MusicStore.requireInstance() val musicStore = MusicStore.requireInstance()
mCurrentGenre.value = musicStore.genres.find { it.id == id } val genre =
refreshGenreData() 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 */ /** Navigate to an item, whether a song/album/artist */
@ -132,38 +140,29 @@ class DetailViewModel : ViewModel() {
isNavigating = navigating isNavigating = navigating
} }
private fun refreshGenreData() { private fun refreshGenreData(genre: Genre) {
logD("Refreshing genre data") logD("Refreshing genre data")
val genre = requireNotNull(currentGenre.value)
val data = mutableListOf<Item>(genre) val data = mutableListOf<Item>(genre)
data.add(SortHeader(-2, R.string.lbl_songs)) data.add(SortHeader(-2, R.string.lbl_songs))
data.addAll(settingsManager.detailGenreSort.genre(currentGenre.value!!)) data.addAll(genreSort.genre(genre))
mGenreData.value = data mGenreData.value = data
} }
private fun refreshArtistData() { private fun refreshArtistData(artist: Artist) {
logD("Refreshing artist data") logD("Refreshing artist data")
val artist = requireNotNull(currentArtist.value)
val data = mutableListOf<Item>(artist) val data = mutableListOf<Item>(artist)
data.add(Header(-2, R.string.lbl_albums)) data.add(Header(-2, R.string.lbl_albums))
data.addAll(Sort.ByYear(false).albums(artist.albums)) data.addAll(Sort.ByYear(false).albums(artist.albums))
data.add(SortHeader(-3, R.string.lbl_songs)) data.add(SortHeader(-3, R.string.lbl_songs))
data.addAll(settingsManager.detailArtistSort.artist(artist)) data.addAll(artistSort.artist(artist))
mArtistData.value = data.toList() mArtistData.value = data.toList()
} }
private fun refreshAlbumData() { private fun refreshAlbumData(album: Album) {
logD("Refreshing album data") logD("Refreshing album data")
val album = requireNotNull(currentAlbum.value)
val data = mutableListOf<Item>(album) val data = mutableListOf<Item>(album)
data.add(SortHeader(id = -2, R.string.lbl_albums)) data.add(SortHeader(id = -2, R.string.lbl_albums))
data.addAll(settingsManager.detailAlbumSort.album(currentAlbum.value!!)) data.addAll(albumSort.album(album))
mAlbumData.value = data 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.applySpans
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* The [DetailFragment] for a genre. * The [DetailFragment] for a genre.
@ -49,7 +50,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setGenreId(args.genreId) detailModel.setGenreId(args.genreId)
setupToolbar(detailModel.currentGenre.value!!) setupToolbar(unlikelyToBeNull(detailModel.currentGenre.value))
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
applySpans { pos -> applySpans { pos ->
@ -83,11 +84,11 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
} }
override fun onPlayParent() { override fun onPlayParent() {
playbackModel.playGenre(requireNotNull(detailModel.currentGenre.value), false) playbackModel.playGenre(unlikelyToBeNull(detailModel.currentGenre.value), false)
} }
override fun onShuffleParent() { override fun onShuffleParent() {
playbackModel.playGenre(requireNotNull(detailModel.currentGenre.value), true) playbackModel.playGenre(unlikelyToBeNull(detailModel.currentGenre.value), true)
} }
override fun onShowSortMenu(anchor: View) { override fun onShowSortMenu(anchor: View) {
@ -119,7 +120,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
private fun updateSong(song: Song?, adapter: GenreDetailAdapter) { private fun updateSong(song: Song?, adapter: GenreDetailAdapter) {
val binding = requireBinding() val binding = requireBinding()
if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE && 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) adapter.highlightSong(song, binding.detailRecycler)
} else { } else {
// Clear the ViewHolders if the mode isn't ALL_SONGS // 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) { override fun bind(item: Album, listener: MenuItemListener) {
binding.parentImage.bindAlbumCover(item) binding.parentImage.bindAlbumCover(item)
binding.parentName.textSafe = item.resolvedName 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 { binding.root.apply {
setOnClickListener { listener.onItemClick(item) } 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.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logTraceOrThrow 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 * 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 -> { R.id.option_sort_asc -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
homeModel.updateCurrentSort( homeModel.updateCurrentSort(
requireNotNull( unlikelyToBeNull(
homeModel homeModel
.getSortForDisplay(homeModel.currentTab.value!!) .getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value))
.ascending(item.isChecked))) .ascending(item.isChecked)))
} }
@ -144,9 +145,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
else -> { else -> {
item.isChecked = true item.isChecked = true
homeModel.updateCurrentSort( homeModel.updateCurrentSort(
requireNotNull( unlikelyToBeNull(
homeModel homeModel
.getSortForDisplay(homeModel.currentTab.value!!) .getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value))
.assignId(item.itemId))) .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.DisplayMode
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state. * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state.
@ -113,19 +114,19 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
when (mCurrentTab.value) { when (mCurrentTab.value) {
DisplayMode.SHOW_SONGS -> { DisplayMode.SHOW_SONGS -> {
settingsManager.libSongSort = sort settingsManager.libSongSort = sort
mSongs.value = sort.songs(mSongs.value!!) mSongs.value = sort.songs(unlikelyToBeNull(mSongs.value))
} }
DisplayMode.SHOW_ALBUMS -> { DisplayMode.SHOW_ALBUMS -> {
settingsManager.libAlbumSort = sort settingsManager.libAlbumSort = sort
mAlbums.value = sort.albums(mAlbums.value!!) mAlbums.value = sort.albums(unlikelyToBeNull(mAlbums.value))
} }
DisplayMode.SHOW_ARTISTS -> { DisplayMode.SHOW_ARTISTS -> {
settingsManager.libArtistSort = sort settingsManager.libArtistSort = sort
mArtists.value = sort.artists(mArtists.value!!) mArtists.value = sort.artists(unlikelyToBeNull(mArtists.value))
} }
DisplayMode.SHOW_GENRES -> { DisplayMode.SHOW_GENRES -> {
settingsManager.libGenreSort = sort settingsManager.libGenreSort = sort
mGenres.value = sort.genres(mGenres.value!!) mGenres.value = sort.genres(unlikelyToBeNull(mGenres.value))
} }
else -> {} else -> {}
} }

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A [HomeListFragment] for showing a list of [Album]s. * A [HomeListFragment] for showing a list of [Album]s.
@ -50,7 +51,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
} }
override fun getPopup(pos: Int): String? { 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. // Change how we display the popup depending on the mode.
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) { 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.PrimitiveBackingData
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A [HomeListFragment] for showing a list of [Artist]s. * A [HomeListFragment] for showing a list of [Artist]s.
@ -48,7 +49,11 @@ class ArtistListFragment : HomeListFragment<Artist>() {
} }
override fun getPopup(pos: Int) = 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) { override fun onItemClick(item: Item) {
check(item is Artist) 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.PrimitiveBackingData
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A [HomeListFragment] for showing a list of [Genre]s. * A [HomeListFragment] for showing a list of [Genre]s.
@ -48,7 +49,11 @@ class GenreListFragment : HomeListFragment<Genre>() {
} }
override fun getPopup(pos: Int) = 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) { override fun onItemClick(item: Item) {
check(item is Genre) 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.Sort
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A [HomeListFragment] for showing a list of [Song]s. * A [HomeListFragment] for showing a list of [Song]s.
@ -48,7 +49,7 @@ class SongListFragment : HomeListFragment<Song>() {
} }
override fun getPopup(pos: Int): String { 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. // 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 // 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.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.util.unlikelyToBeNull
// --- MUSIC MODELS --- // --- MUSIC MODELS ---
@ -84,19 +85,16 @@ data class Song(
/** The duration of this song, in seconds (rounded down) */ /** The duration of this song, in seconds (rounded down) */
val seconds: Long val seconds: Long
get() = duration / 1000 get() = duration / 1000
/** The seconds of this song, but as a duration. */
val formattedDuration: String
get() = seconds.toDuration(false)
private var mAlbum: Album? = null private var mAlbum: Album? = null
/** The album of this song. */ /** The album of this song. */
val album: Album val album: Album
get() = requireNotNull(mAlbum) get() = unlikelyToBeNull(mAlbum)
private var mGenre: Genre? = null private var mGenre: Genre? = null
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */ /** The genre of this song. Will be an "unknown genre" if the song does not have any. */
val genre: Genre val genre: Genre
get() = requireNotNull(mGenre) get() = unlikelyToBeNull(mGenre)
/** An album name resolved to this song in particular. */ /** An album name resolved to this song in particular. */
val resolvedAlbumName: String val resolvedAlbumName: String
@ -177,7 +175,7 @@ data class Album(
private var mArtist: Artist? = null private var mArtist: Artist? = null
/** The parent artist of this album. */ /** The parent artist of this album. */
val artist: Artist val artist: Artist
get() = requireNotNull(mArtist) get() = unlikelyToBeNull(mArtist)
/** The artist name, resolved to this album in particular. */ /** The artist name, resolved to this album in particular. */
val resolvedArtistName: String val resolvedArtistName: String

View file

@ -18,7 +18,6 @@
package org.oxycblt.auxio.music.excluded package org.oxycblt.auxio.music.excluded
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.DiffUtil
import org.oxycblt.auxio.databinding.ItemExcludedDirBinding import org.oxycblt.auxio.databinding.ItemExcludedDirBinding
import org.oxycblt.auxio.ui.BindingViewHolder import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.MonoAdapter 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) : class ExcludedViewHolder private constructor(private val binding: ItemExcludedDirBinding) :
BindingViewHolder<String, ExcludedAdapter.Listener>(binding.root) { BindingViewHolder<String, ExcludedAdapter.Listener>(binding.root) {
override fun bind(item: String, listener: ExcludedAdapter.Listener) { 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.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.util.logD 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 * 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. * called.
*/ */
fun addPath(path: String) { fun addPath(path: String) {
if (!mPaths.value!!.contains(path)) { val paths = unlikelyToBeNull(mPaths.value)
mPaths.value!!.add(path) if (!paths.contains(path)) {
paths.add(path)
mPaths.value = mPaths.value mPaths.value = mPaths.value
isModified = true isModified = true
} }
@ -64,7 +66,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
* [save] is called. * [save] is called.
*/ */
fun removePath(path: String) { fun removePath(path: String) {
mPaths.value!!.remove(path) unlikelyToBeNull(mPaths.value).remove(path)
mPaths.value = mPaths.value mPaths.value = mPaths.value
isModified = true isModified = true
} }
@ -73,7 +75,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
fun save(onDone: () -> Unit) { fun save(onDone: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
excludedDatabase.writePaths(mPaths.value!!) excludedDatabase.writePaths(unlikelyToBeNull(mPaths.value))
isModified = false isModified = false
onDone() onDone()
this@ExcludedViewModel.logD( this@ExcludedViewModel.logD(

View file

@ -107,12 +107,10 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
} }
} }
binding.playbackPlayPause.isActivated = playbackModel.isPlaying.value!!
playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying -> playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying ->
binding.playbackPlayPause.isActivated = isPlaying binding.playbackPlayPause.isActivated = isPlaying
} }
binding.playbackProgressBar.progress = playbackModel.positionSeconds.value!!.toInt()
playbackModel.positionSeconds.observe(viewLifecycleOwner) { position -> playbackModel.positionSeconds.observe(viewLifecycleOwner) { position ->
binding.playbackProgressBar.progress = position.toInt() 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.settings.SettingsManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* The ViewModel that provides a UI frontend for [PlaybackStateManager]. * 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. * is called just before the change is committed so that the adapter can be updated.
*/ */
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) { 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) { if (index in playbackManager.queue.indices) {
apply() apply()
playbackManager.removeQueueItem(index) 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. * is called just before the change is committed so that the adapter can be updated.
*/ */
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean { 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 from = adapterFrom + delta
val to = adapterTo + delta val to = adapterTo + delta
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) { 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.getSystemServiceSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Manages the current volume and playback state across ReplayGain and AudioFocus events. * 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) { 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.home.tabs.TabCustomizeDialog
import org.oxycblt.auxio.music.excluded.ExcludedDialog import org.oxycblt.auxio.music.excluded.ExcludedDialog
import org.oxycblt.auxio.playback.PlaybackViewModel 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.IntListPreference
import org.oxycblt.auxio.settings.pref.IntListPreferenceDialog
import org.oxycblt.auxio.util.hardRestart import org.oxycblt.auxio.util.hardRestart
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -74,9 +74,14 @@ class SettingsListFragment : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.prefs_main, rootKey) setPreferencesFromResource(R.xml.prefs_main, rootKey)
} }
@Suppress("Deprecation")
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference is IntListPreference) { 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 { } else {
super.onDisplayPreferenceDialog(preference) 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.playback.system.ReplayGainMode
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Wrapper around the [SharedPreferences] class that writes & reads values without a context. * 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> var libTabs: Array<Tab>
get() = get() =
Tab.fromSequence(prefs.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) Tab.fromSequence(prefs.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT))
?: Tab.fromSequence(Tab.SEQUENCE_DEFAULT)!! ?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT))
set(value) { set(value) {
prefs.edit { prefs.edit {
putInt(KEY_LIB_TABS, Tab.toSequence(value)) 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.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.preference.PreferenceDialogFragmentCompat
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
/** The dialog shown whenever an [IntListPreference] is shown. */ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() {
class IntListPrefDialog : DialogFragment() { private val listPreference: IntListPreference
get() = (preference as IntListPreference)
private var pendingValueIndex = -1
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = MaterialAlertDialogBuilder(requireActivity(), theme) // PreferenceDialogFragmentCompat does not allow us to customize the actual creation
// of the alert dialog, so we have to manually override onCreateDialog and customize it
// Since we have to store the preference key as an argument, we have to find the // ourselves.
// preference we need to use manually. val builder = MaterialAlertDialogBuilder(requireContext(), theme)
val pref = builder.setTitle(listPreference.title)
requireNotNull( builder.setPositiveButton(null, null)
(parentFragment as PreferenceFragmentCompat).preferenceManager.findPreference< builder.setNegativeButton(android.R.string.cancel, null)
IntListPreference>(requireArguments().getString(ARG_KEY, null))) builder.setSingleChoiceItems(listPreference.entries, listPreference.getValueIndex()) {
_,
builder.setTitle(pref.title) index ->
pendingValueIndex = index
builder.setSingleChoiceItems(pref.entries, pref.getValueIndex()) { _, index ->
pref.setValueIndex(index)
dismiss() dismiss()
} }
builder.setNegativeButton(android.R.string.cancel, null)
return builder.create() return builder.create()
} }
override fun onDialogClosed(positiveResult: Boolean) {
if (pendingValueIndex > -1) {
listPreference.setValueIndex(pendingValueIndex)
}
}
companion object { companion object {
const val TAG = BuildConfig.APPLICATION_ID + ".tag.INT_PREF" const val TAG = BuildConfig.APPLICATION_ID + ".tag.INT_PREF"
const val ARG_KEY = BuildConfig.APPLICATION_ID + ".arg.PREF_KEY"
fun from(pref: IntListPreference): IntListPrefDialog { fun from(pref: IntListPreference): IntListPreferenceDialog {
return IntListPrefDialog().apply { return IntListPreferenceDialog().apply {
arguments = Bundle().apply { putString(ARG_KEY, pref.key) } arguments = Bundle().apply { putString(ARG_KEY, pref.key) }
} }
} }

View file

@ -33,9 +33,6 @@ constructor(
defStyleRes: Int = 0 defStyleRes: Int = 0
) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { ) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
// Reflect into Preference to get the (normally inaccessible) default value. // 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 entries: Array<CharSequence>
val values: IntArray val values: IntArray
private var currentValue: Int? = null private var currentValue: Int? = null
@ -108,4 +105,9 @@ constructor(
return "<not set>" 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?> { class NullableComparator<T : Comparable<T>> : Comparator<T?> {
override fun compare(a: T?, b: T?): Int { override fun compare(a: T?, b: T?): Int {
if (a == null && b != null) return -1 // -1 -> a < b return when {
if (a == null && b == null) return 0 // 0 -> a = b a != null && b != null -> a.compareTo(b)
if (a != null && b == null) return 1 // 1 -> a > b a == null && b != null -> -1 // a < b
return a!!.compareTo(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 { fun Context.getColorStateListSafe(@ColorRes color: Int): ColorStateList {
return try { return try {
requireNotNull(ContextCompat.getColorStateList(this, color)) unlikelyToBeNull(ContextCompat.getColorStateList(this, color))
} catch (e: Exception) { } catch (e: Exception) {
handleResourceFailure(e, "color state list", getColorSafe(android.R.color.black).stateList) 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.database.sqlite.SQLiteDatabase
import android.os.Looper import android.os.Looper
import androidx.fragment.app.Fragment 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 * 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) { * Sanitizes a nullable value that is not likely to be null. On debug builds, requireNotNull is
error("Fragment is detached from activity") * 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.isLandscape
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW 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 * 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") logW("No good widget layout found")
val minimum = 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) updateAppWidget(id, minimum)
} }

View file

@ -2,7 +2,7 @@
<resources> <resources>
<integer name="detail_app_bar_title_anim_duration">150</integer> <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 --> <!-- Preference values -->
<string-array name="entries_theme"> <string-array name="entries_theme">
<item>@string/set_theme_auto</item> <item>@string/set_theme_auto</item>