diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt b/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt index ca387c0..521a1fe 100644 --- a/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt @@ -8,6 +8,8 @@ import de.jrpie.android.launcher.actions.AppAction import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.ShortcutAction import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.util.countOccurrences +import de.jrpie.android.launcher.util.isSubsequent import java.util.Locale import kotlin.text.Regex.Companion.escape @@ -22,7 +24,6 @@ class AppFilter( operator fun invoke(apps: List): List { var apps = apps.sortedBy { app -> app.getCustomLabel(context).lowercase(Locale.ROOT) } - val hidden = LauncherPreferences.apps().hidden() ?: setOf() val favorites = LauncherPreferences.apps().favorites() ?: setOf() val private = apps.filter { it.isPrivate() } @@ -62,25 +63,40 @@ class AppFilter( if (query.isEmpty()) { return apps - } else { - val r: MutableList = ArrayList() - val appsSecondary: MutableList = ArrayList() - val normalizedQuery: String = normalize(query) - for (item in apps) { - val itemLabel: String = normalize(item.getCustomLabel(context)) - - if (itemLabel.startsWith(normalizedQuery)) { - r.add(item) - } else if (itemLabel.contains(normalizedQuery)) { - appsSecondary.add(item) - } - } - r.addAll(appsSecondary) - - return r } + val r: MutableSet = hashSetOf() + val normalizedQuery: String = normalize(query) + val subsequentResult: MutableList = mutableListOf() + val occurrences: MutableMap = mutableMapOf() + for (item in apps) { + val itemLabel: String = normalize(item.getCustomLabel(context)) + + if (itemLabel.startsWith(normalizedQuery)) { + r.add(item) + } else if (itemLabel.contains(normalizedQuery)) { + r.add(item) + } + if (LauncherPreferences.functionality().searchFuzzy()) { + if (isSubsequent(itemLabel, normalizedQuery)) { + subsequentResult.add(item) + } + occurrences[item] = countOccurrences(itemLabel, normalizedQuery) + } + } + if (LauncherPreferences.functionality().searchFuzzy() && r.size != 1) { + if (subsequentResult.isNotEmpty()) { + r.addAll(subsequentResult) + } else { + val maxOccurrences = occurrences.values.maxOrNull() + if (maxOccurrences == 0) return apps + val result = occurrences.filter { it.value == maxOccurrences } + r.addAll(result.keys) + } + } + return r.toList().sortedBy { it.getCustomLabel(context).lowercase(Locale.ROOT) } } + companion object { enum class AppSetVisibility( val predicate: (set: Set, AbstractDetailedAppInfo) -> Boolean diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index d509ef2..e73e548 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -75,6 +75,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "search_web", type = boolean.class, description = "false"), @Preference(name = "search_auto_open_keyboard", type = boolean.class, defaultValue = "true"), @Preference(name = "search_auto_close_keyboard", type = boolean.class, defaultValue = "false"), + @Preference(name = "search_fuzzy", type = boolean.class, defaultValue = "true"), }), @PreferenceGroup(name = "enabled_gestures", prefix = "settings_enabled_gestures_", suffix = "_key", value = { @Preference(name = "double_swipe", type = boolean.class, defaultValue = "true"), diff --git a/app/src/main/java/de/jrpie/android/launcher/util/Algorithms.kt b/app/src/main/java/de/jrpie/android/launcher/util/Algorithms.kt new file mode 100644 index 0000000..6e7151c --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/util/Algorithms.kt @@ -0,0 +1,39 @@ +package de.jrpie.android.launcher.util + +/** + * Returns true if `search` is a subsequence of `text`. + * A subsequence means all characters in `search` appear in `text` + * in the same order, but not necessarily contiguously. + */ +fun isSubsequent(text: String, search: String): Boolean { + var i = 0 + for (char in text) { + if (char != search[i]) continue + i++ + if (i == search.length) { + return true + } + } + return false +} + +/** + * Returns the amount of characters from `search` that occur inside `text`. + * If `text` contains the same character multiple times, it is only counted + * as often as it occurs in `search`. + */ +fun countOccurrences(text: String, search: String): Int { + val frequencies = mutableMapOf() + for (char in text) { + frequencies[char] = frequencies.getOrElse(char) { 0 } + 1 + } + var result = 0 + for (char in search) { + val charFrequency = frequencies[char] ?: 0 + if (charFrequency > 0) { + result++ + frequencies[char] = charFrequency - 1 + } + } + return result +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0b4090c..82d6474 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -309,4 +309,5 @@ Privater Bereich Musik: Wiedergabe / Pause Appliste umkehren + Fuzzy-Suche verwenden diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 9f15b22..d23e5c6 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -148,6 +148,7 @@ enabled_gestures.edge_actions.edge_width functionality.search_auto_launch functionality.search_web + functionality.search_fuzzy functionality.search_auto_keyboard functionality.search_auto_close_keyboard diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6fb9d61..0cd98dd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -456,4 +456,5 @@ Navigate next Lock Remove binding + Use fuzzy search diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 0ee7c17..c100e32 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -100,6 +100,11 @@ android:defaultValue="false" android:summary="@string/settings_functionality_search_web_summary" /> +