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

View file

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

View file

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

View file

@ -17,8 +17,10 @@
package org.oxycblt.auxio.search package org.oxycblt.auxio.search
import android.app.Application
import android.content.Context import android.content.Context
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.text.Normalizer 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.Header
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
* The [ViewModel] for search functionality * The [ViewModel] for search functionality.
* @author OxygenCobalt * @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 musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.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]. * 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 lastQuery = query
val library = musicStore.library val library = musicStore.library
@ -84,28 +85,28 @@ class SearchViewModel : ViewModel() {
// Note: a filter mode of null means to not filter at all. // Note: a filter mode of null means to not filter at all.
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ARTISTS) { 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.add(Header(-1, R.string.lbl_artists))
results.addAll(sort.artists(artists)) results.addAll(sort.artists(artists))
} }
} }
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ALBUMS) { 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.add(Header(-2, R.string.lbl_albums))
results.addAll(sort.albums(albums)) results.addAll(sort.albums(albums))
} }
} }
if (_filterMode == null || _filterMode == DisplayMode.SHOW_GENRES) { 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.add(Header(-3, R.string.lbl_genres))
results.addAll(sort.genres(genres)) results.addAll(sort.genres(genres))
} }
} }
if (_filterMode == null || _filterMode == DisplayMode.SHOW_SONGS) { 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.add(Header(-4, R.string.lbl_songs))
results.addAll(sort.songs(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]. * 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 = _filterMode =
when (id) { when (id) {
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
@ -137,20 +133,20 @@ class SearchViewModel : ViewModel() {
settingsManager.searchFilterMode = _filterMode 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 * Shortcut that will run a ignoreCase filter on a list and only return a value if the resulting
* list is empty. * 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 { val filtered = filter {
// Compare normalized names, which are names with unicode characters that are // Compare normalized names, which are names with unicode characters that are
// normalized to their non-unicode forms. This is just for quality-of-life, // 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. // and I hope it doesn't bork search functionality for other languages.
it.resolveNameNormalized(context).contains(value, ignoreCase = true) || it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
it.resolveNameNormalized(context).contains(value, ignoreCase = true) it.resolveNameNormalized(application).contains(value, ignoreCase = true)
} }
return filtered.ifEmpty { null } return filtered.ifEmpty { null }
@ -185,4 +181,16 @@ class SearchViewModel : ViewModel() {
return sb.toString() 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 package org.oxycblt.auxio.util
import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.database.Cursor import android.database.Cursor
@ -31,7 +32,11 @@ import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment 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.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@ -168,6 +173,20 @@ fun Fragment.launch(
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) } 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 * 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 * 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> <string name="cdc_wav">Microsoft WAVE</string>
<!-- Note: These are stopgap measures until we make the path code rely on components! --> <!-- 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_primary_path">Internal/%s</string>
<string name="fmt_secondary_path">SDCARD:%s</string> <string name="fmt_secondary_path">SDCARD/%s</string>
</resources> </resources>