diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt
new file mode 100644
index 000000000..831f758f9
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.search
+
+import android.content.Context
+import java.text.Normalizer
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.Song
+
+/**
+ * Implements the fuzzy-ish searching algorithm used in the search view.
+ * @author Alexander Capehart
+ */
+interface SearchEngine {
+ /**
+ * Begin a search.
+ * @param items The items to search over.
+ * @param query The query to search for.
+ * @return A list of items filtered by the given query.
+ */
+ suspend fun search(items: Items, query: String): Items
+
+ /**
+ * Input/output data to use with [SearchEngine].
+ * @param songs A list of [Song]s, null if empty.
+ * @param albums A list of [Album]s, null if empty.
+ * @param artists A list of [Artist]s, null if empty.
+ * @param genres A list of [Genre]s, null if empty.
+ */
+ data class Items(
+ val songs: List?,
+ val albums: List?,
+ val artists: List?,
+ val genres: List?
+ )
+
+ private class Real(private val context: Context) : SearchEngine {
+ override suspend fun search(items: Items, query: String) =
+ Items(
+ songs =
+ items.songs?.searchListImpl(query) { q, song -> song.path.name.contains(q) },
+ albums = items.albums?.searchListImpl(query),
+ artists = items.artists?.searchListImpl(query),
+ genres = items.genres?.searchListImpl(query))
+
+ /**
+ * Search a given [Music] list.
+ * @param query The query to search for. The routine will compare this query to the names of
+ * each object in the list and
+ * @param fallback Additional comparison code to run if the item does not match the query
+ * initially. This can be used to compare against additional attributes to improve search
+ * result quality.
+ */
+ private inline fun List.searchListImpl(
+ query: String,
+ fallback: (String, T) -> Boolean = { _, _ -> false }
+ ) =
+ filter {
+ // See if the plain resolved name matches the query. This works for most
+ // situations.
+ val name = it.resolveName(context)
+ if (name.contains(query, ignoreCase = true)) {
+ return@filter true
+ }
+
+ // See if the sort name matches. This can sometimes be helpful as certain
+ // libraries
+ // will tag sort names to have a alphabetized version of the title.
+ val sortName = it.rawSortName
+ if (sortName != null && sortName.contains(query, ignoreCase = true)) {
+ return@filter true
+ }
+
+ // As a last-ditch effort, see if the normalized name matches. This will replace
+ // any non-alphabetical characters with their alphabetical representations,
+ // which
+ // could make it match the query.
+ val normalizedName =
+ NORMALIZATION_SANITIZE_REGEX.replace(
+ Normalizer.normalize(name, Normalizer.Form.NFKD), "")
+ if (normalizedName.contains(query, ignoreCase = true)) {
+ return@filter true
+ }
+
+ fallback(query, it)
+ }
+ .ifEmpty { null }
+
+ private companion object {
+ /**
+ * Converts the output of [Normalizer] to remove any junk characters added by it's
+ * replacements.
+ */
+ val NORMALIZATION_SANITIZE_REGEX = Regex("\\p{InCombiningDiacriticalMarks}+")
+ }
+ }
+
+ companion object {
+ /**
+ * Get a framework-backed implementation.
+ * @param context [Context] required.
+ */
+ fun from(context: Context): SearchEngine = Real(context)
+ }
+}
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 9341a7390..7a6b6f968 100644
--- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt
@@ -21,7 +21,6 @@ import android.app.Application
import androidx.annotation.IdRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
-import java.text.Normalizer
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -35,7 +34,6 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.PlaybackSettings
-import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD
/**
@@ -47,6 +45,7 @@ class SearchViewModel(application: Application) :
private val musicStore = MusicStore.getInstance()
private val searchSettings = SearchSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
+ private var searchEngine = SearchEngine.from(application)
private var lastQuery: String? = null
private var currentSearchJob: Job? = null
@@ -101,87 +100,43 @@ class SearchViewModel(application: Application) :
}
}
- private fun searchImpl(library: Library, query: String): List- {
- val sort = Sort(Sort.Mode.ByName, true)
+ private suspend fun searchImpl(library: Library, query: String): List
- {
val filterMode = searchSettings.searchFilterMode
- val results = mutableListOf
- ()
- // Note: A null filter mode maps to the "All" filter option, hence the check.
+ val items =
+ if (filterMode == null) {
+ // A nulled filter mode means to not filter anything.
+ SearchEngine.Items(library.songs, library.albums, library.artists, library.genres)
+ } else {
+ SearchEngine.Items(
+ songs = if (filterMode == MusicMode.SONGS) library.songs else null,
+ albums = if (filterMode == MusicMode.ALBUMS) library.albums else null,
+ artists = if (filterMode == MusicMode.ARTISTS) library.artists else null,
+ genres = if (filterMode == MusicMode.GENRES) library.genres else null)
+ }
- if (filterMode == null || filterMode == MusicMode.ARTISTS) {
- library.artists.searchListImpl(query)?.let {
- results.add(Header(R.string.lbl_artists))
- results.addAll(sort.artists(it))
+ val results = searchEngine.search(items, query)
+
+ return buildList {
+ results.artists?.let { artists ->
+ add(Header(R.string.lbl_artists))
+ addAll(SORT.artists(artists))
+ }
+ results.albums?.let { albums ->
+ add(Header(R.string.lbl_albums))
+ addAll(SORT.albums(albums))
+ }
+ results.genres?.let { genres ->
+ add(Header(R.string.lbl_genres))
+ addAll(SORT.genres(genres))
+ }
+ results.songs?.let { songs ->
+ add(Header(R.string.lbl_songs))
+ addAll(SORT.songs(songs))
}
}
-
- if (filterMode == null || filterMode == MusicMode.ALBUMS) {
- library.albums.searchListImpl(query)?.let {
- results.add(Header(R.string.lbl_albums))
- results.addAll(sort.albums(it))
- }
- }
-
- if (filterMode == null || filterMode == MusicMode.GENRES) {
- library.genres.searchListImpl(query)?.let {
- results.add(Header(R.string.lbl_genres))
- results.addAll(sort.genres(it))
- }
- }
-
- if (filterMode == null || filterMode == MusicMode.SONGS) {
- library.songs
- .searchListImpl(query) { q, song -> song.path.name.contains(q) }
- ?.let {
- results.add(Header(R.string.lbl_songs))
- results.addAll(sort.songs(it))
- }
- }
-
- // Handle if we were canceled while searching.
- return results
}
- /**
- * Search a given [Music] list.
- * @param query The query to search for. The routine will compare this query to the names of
- * each object in the list and
- * @param fallback Additional comparison code to run if the item does not match the query
- * initially. This can be used to compare against additional attributes to improve search result
- * quality.
- */
- private inline fun List.searchListImpl(
- query: String,
- fallback: (String, T) -> Boolean = { _, _ -> false }
- ) =
- filter {
- // See if the plain resolved name matches the query. This works for most situations.
- val name = it.resolveName(context)
- if (name.contains(query, ignoreCase = true)) {
- return@filter true
- }
-
- // See if the sort name matches. This can sometimes be helpful as certain libraries
- // will tag sort names to have a alphabetized version of the title.
- val sortName = it.rawSortName
- if (sortName != null && sortName.contains(query, ignoreCase = true)) {
- return@filter true
- }
-
- // As a last-ditch effort, see if the normalized name matches. This will replace
- // any non-alphabetical characters with their alphabetical representations, which
- // could make it match the query.
- val normalizedName =
- NORMALIZATION_SANITIZE_REGEX.replace(
- Normalizer.normalize(name, Normalizer.Form.NFKD), "")
- if (normalizedName.contains(query, ignoreCase = true)) {
- return@filter true
- }
-
- fallback(query, it)
- }
- .ifEmpty { null }
-
/**
* Returns the ID of the filter option to currently highlight.
* @return A menu item ID of the filtering option selected.
@@ -218,10 +173,6 @@ class SearchViewModel(application: Application) :
}
private companion object {
- /**
- * Converts the output of [Normalizer] to remove any junk characters added by it's
- * replacements.
- */
- val NORMALIZATION_SANITIZE_REGEX = Regex("\\p{InCombiningDiacriticalMarks}+")
+ val SORT = Sort(Sort.Mode.ByName, true)
}
}