From 107f7bee275b020607cfafb62bc9ba76f7710d96 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Mon, 13 Jun 2022 12:07:34 -0600 Subject: [PATCH] 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. --- .../oxycblt/auxio/detail/DetailViewModel.kt | 32 +++++++------ .../oxycblt/auxio/detail/SongDetailDialog.kt | 2 +- .../oxycblt/auxio/search/SearchFragment.kt | 18 ++----- .../oxycblt/auxio/search/SearchViewModel.kt | 48 +++++++++++-------- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 19 ++++++++ app/src/main/res/values/donottranslate.xml | 4 +- 6 files changed, 71 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index e15aba997..8ef9e9d40 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index 5a9cb2215..bb3aa8c1a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -44,7 +44,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { 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) } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 00d3b5e38..3393ffaab 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index e23cdb3cc..359e6841b 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -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 List.filterByOrNull(context: Context, value: String): List? { + private fun List.filterByOrNull(value: String): List? { 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) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index a73b549c4..126392280 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -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 Fragment.androidViewModels() = + viewModels { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) } + +inline fun Fragment.activityAndroidViewModels() = + activityViewModels { + 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 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 2832c3934..e67fe1771 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -14,6 +14,6 @@ Microsoft WAVE - Internal:%s - SDCARD:%s + Internal/%s + SDCARD/%s \ No newline at end of file