all: add viewmodel contexts where useful

Make some ViewModel instances AndroidViewModels in order to make some
code less insane.

I don't like doing this, as I want to keep ViewModel instances clean of
android things, but this just makes a lot of functionality easier to
implement.
This commit is contained in:
OxygenCobalt 2022-06-13 12:07:34 -06:00
parent 543a3ebffb
commit 107f7bee27
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 71 additions and 52 deletions

View file

@ -17,10 +17,10 @@
package org.oxycblt.auxio.detail
import android.content.Context
import android.app.Application
import android.media.MediaExtractor
import android.media.MediaFormat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -40,6 +40,7 @@ import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -51,7 +52,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* - Menu triggers for each fragment
* @author OxygenCobalt
*/
class DetailViewModel : ViewModel(), MusicStore.Callback {
class DetailViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Callback {
data class DetailSong(
val song: Song,
val bitrateKbps: Int?,
@ -109,11 +111,11 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
currentGenre.value?.let(::refreshGenreData)
}
fun setSongId(context: Context, id: Long) {
fun setSongId(id: Long) {
if (_currentSong.value?.run { song.id } == id) return
val library = unlikelyToBeNull(musicStore.library)
val song = requireNotNull(library.songs.find { it.id == id }) { "Invalid song id provided" }
generateDetailSong(context, song)
generateDetailSong(song)
}
fun clearSong() {
@ -152,14 +154,15 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
musicStore.addCallback(this)
}
private fun generateDetailSong(context: Context, song: Song) {
private fun generateDetailSong(song: Song) {
viewModelScope.launch {
_currentSong.value =
withContext(Dispatchers.IO) {
val extractor = MediaExtractor()
try {
extractor.setDataSource(context, song.uri, emptyMap())
@Suppress("BlockingMethodInNonBlockingContext")
extractor.setDataSource(application, song.uri, emptyMap())
} catch (e: Exception) {
logW("Unable to extract song attributes.")
logW(e.stackTraceToString())
@ -250,14 +253,13 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
override fun onLibraryChanged(library: MusicStore.Library?) {
if (library != null) {
// TODO: Add when we have a context
// val song = currentSong.value
// if (song != null) {
// val newSong = library.sanitize(song.song)
// if (newSong != null) {
// generateDetailSong(newSong)
// }
// }
val song = currentSong.value
if (song != null) {
val newSong = library.sanitize(song.song)
if (newSong != null) {
generateDetailSong(newSong)
}
}
val album = currentAlbum.value
if (album != null) {

View file

@ -44,7 +44,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
detailModel.setSongId(requireContext(), requireNotNull(arguments).getLong(ARG_ID))
detailModel.setSongId(requireNotNull(arguments).getLong(ARG_ID))
launch { detailModel.currentSong.collect(::updateSong) }
}

View file

@ -28,15 +28,12 @@ import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Indexer
import org.oxycblt.auxio.music.IndexerViewModel
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
@ -47,6 +44,7 @@ import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.androidViewModels
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.launch
@ -61,10 +59,9 @@ class SearchFragment :
MenuItemListener,
Toolbar.OnMenuItemClickListener {
// SearchViewModel is only scoped to this Fragment
private val searchModel: SearchViewModel by viewModels()
private val searchModel: SearchViewModel by androidViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val indexerModel: IndexerViewModel by activityViewModels()
private val searchAdapter = SearchAdapter(this)
private var imm: InputMethodManager? = null
@ -87,7 +84,7 @@ class SearchFragment :
binding.searchEditText.apply {
addTextChangedListener { text ->
// Run the search with the updated text as the query
searchModel.search(context, text?.toString())
searchModel.search(text?.toString())
}
if (!launchedKeyboard) {
@ -109,7 +106,6 @@ class SearchFragment :
// --- VIEWMODEL SETUP ---
launch { searchModel.searchResults.collect(::updateResults) }
launch { indexerModel.state.collect(::handleIndexerState) }
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
}
@ -124,7 +120,7 @@ class SearchFragment :
R.id.submenu_filtering -> {}
else -> {
if (item.itemId != R.id.submenu_filtering) {
searchModel.updateFilterModeWithId(requireContext(), item.itemId)
searchModel.updateFilterModeWithId(item.itemId)
item.isChecked = true
}
}
@ -171,12 +167,6 @@ class SearchFragment :
requireImm().hide()
}
private fun handleIndexerState(state: Indexer.State?) {
if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) {
searchModel.refresh(requireContext())
}
}
private fun requireImm(): InputMethodManager {
requireAttached()
val instance = imm

View file

@ -17,8 +17,10 @@
package org.oxycblt.auxio.search
import android.app.Application
import android.content.Context
import androidx.annotation.IdRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.text.Normalizer
@ -33,16 +35,15 @@ import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD
/**
* The [ViewModel] for search functionality
* The [ViewModel] for search functionality.
* @author OxygenCobalt
*
* TODO: Add a context to this ViewModel, not because I want to, but because it just makes the code
* easier to work with.
*/
class SearchViewModel : ViewModel() {
class SearchViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Callback {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
@ -64,7 +65,7 @@ class SearchViewModel : ViewModel() {
/**
* Use [query] to perform a search of the music library. Will push results to [searchResults].
*/
fun search(context: Context, query: String?) {
fun search(query: String?) {
lastQuery = query
val library = musicStore.library
@ -84,28 +85,28 @@ class SearchViewModel : ViewModel() {
// Note: a filter mode of null means to not filter at all.
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ARTISTS) {
library.artists.filterByOrNull(context, query)?.let { artists ->
library.artists.filterByOrNull(query)?.let { artists ->
results.add(Header(-1, R.string.lbl_artists))
results.addAll(sort.artists(artists))
}
}
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ALBUMS) {
library.albums.filterByOrNull(context, query)?.let { albums ->
library.albums.filterByOrNull(query)?.let { albums ->
results.add(Header(-2, R.string.lbl_albums))
results.addAll(sort.albums(albums))
}
}
if (_filterMode == null || _filterMode == DisplayMode.SHOW_GENRES) {
library.genres.filterByOrNull(context, query)?.let { genres ->
library.genres.filterByOrNull(query)?.let { genres ->
results.add(Header(-3, R.string.lbl_genres))
results.addAll(sort.genres(genres))
}
}
if (_filterMode == null || _filterMode == DisplayMode.SHOW_SONGS) {
library.songs.filterByOrNull(context, query)?.let { songs ->
library.songs.filterByOrNull(query)?.let { songs ->
results.add(Header(-4, R.string.lbl_songs))
results.addAll(sort.songs(songs))
}
@ -115,15 +116,10 @@ class SearchViewModel : ViewModel() {
}
}
/** Re-search the library using the last query. Will push results to [searchResults]. */
fun refresh(context: Context) {
search(context, lastQuery)
}
/**
* Update the current filter mode with a menu [id]. New value will be pushed to [filterMode].
*/
fun updateFilterModeWithId(context: Context, @IdRes id: Int) {
fun updateFilterModeWithId(@IdRes id: Int) {
_filterMode =
when (id) {
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
@ -137,20 +133,20 @@ class SearchViewModel : ViewModel() {
settingsManager.searchFilterMode = _filterMode
refresh(context)
search(lastQuery)
}
/**
* Shortcut that will run a ignoreCase filter on a list and only return a value if the resulting
* list is empty.
*/
private fun <T : Music> List<T>.filterByOrNull(context: Context, value: String): List<T>? {
private fun <T : Music> List<T>.filterByOrNull(value: String): List<T>? {
val filtered = filter {
// Compare normalized names, which are names with unicode characters that are
// normalized to their non-unicode forms. This is just for quality-of-life,
// and I hope it doesn't bork search functionality for other languages.
it.resolveNameNormalized(context).contains(value, ignoreCase = true) ||
it.resolveNameNormalized(context).contains(value, ignoreCase = true)
it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
it.resolveNameNormalized(application).contains(value, ignoreCase = true)
}
return filtered.ifEmpty { null }
@ -185,4 +181,16 @@ class SearchViewModel : ViewModel() {
return sb.toString()
}
override fun onLibraryChanged(library: MusicStore.Library?) {
if (library != null) {
// Make sure our query is up to date with the music library.
search(lastQuery)
}
}
override fun onCleared() {
super.onCleared()
musicStore.removeCallback(this)
}
}

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.util
import android.app.Application
import android.content.Context
import android.content.res.ColorStateList
import android.database.Cursor
@ -31,7 +32,11 @@ import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
@ -168,6 +173,20 @@ fun Fragment.launch(
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
}
fun Fragment.androidViewModelFactory() =
ViewModelProvider.AndroidViewModelFactory(requireContext().applicationContext as Application)
inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) }
inline fun <reified T : AndroidViewModel> Fragment.activityAndroidViewModels() =
activityViewModels<T> {
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
}
val AndroidViewModel.application: Application
get() = getApplication()
/**
* Combines the called flow with the given flow and then collects them both into [block]. This is a
* bit of a dumb hack with [combine], as when we have to combine flows, we often just want to call

View file

@ -14,6 +14,6 @@
<string name="cdc_wav">Microsoft WAVE</string>
<!-- Note: These are stopgap measures until we make the path code rely on components! -->
<string name="fmt_primary_path">Internal:%s</string>
<string name="fmt_secondary_path">SDCARD:%s</string>
<string name="fmt_primary_path">Internal/%s</string>
<string name="fmt_secondary_path">SDCARD/%s</string>
</resources>