From 49d09c01c75983d5e8536be1974ae9333a4b1590 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:51:18 +0100 Subject: [PATCH] Optimize string normalization --- .../mm20/launcher2/search/StringNormalizer.kt | 6 ++++ .../launcher2/applications/AppRepository.kt | 15 ++++++-- .../launcher2/applications/LauncherApp.kt | 7 ++++ .../appshortcuts/AppShortcutRepository.kt | 3 +- .../launcher2/data/CompatStringNormalizer.kt | 3 ++ .../launcher2/data/IcuStringNormalizer.kt | 35 +++++++++++-------- 6 files changed, 50 insertions(+), 19 deletions(-) diff --git a/core/base/src/main/java/de/mm20/launcher2/search/StringNormalizer.kt b/core/base/src/main/java/de/mm20/launcher2/search/StringNormalizer.kt index 5c0642dad..5218520fc 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/StringNormalizer.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/StringNormalizer.kt @@ -1,5 +1,11 @@ package de.mm20.launcher2.search interface StringNormalizer { + /** + * A unique identifier for the normalization algorithm. Two normalizers that share the same ID must + * return the same normalized string for the same input. + */ + val id: String + fun normalize(input: String): String } \ No newline at end of file diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt index 2bf3688e6..ca5a57e9b 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt @@ -240,21 +240,30 @@ internal class AppRepositoryImpl( override fun search(query: String, allowNetwork: Boolean): Flow> { val normalizedQuery = stringNormalizer.normalize(query) + return installedApps.map { apps -> withContext(Dispatchers.Default) { + val normalizerId = stringNormalizer.id val appResults = mutableListOf() if (query.isEmpty()) { appResults.addAll(apps) } else { - appResults.addAll(apps.mapNotNull { + appResults.addAll(apps.mapNotNull { app -> + val cachedLabel = app.cachedNormalizerResult val score = ResultScore.from( query = normalizedQuery, primaryFields = listOf( - stringNormalizer.normalize(it.label) + if (cachedLabel?.first == normalizerId) { + cachedLabel.second + } else { + stringNormalizer.normalize(app.label).also { + app.cachedNormalizerResult = normalizerId to it + } + } ), ) if (score.score < 0.8f) return@mapNotNull null - it.copy( + app.copy( score = score ) }) diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt index 4e2b97747..39fa6c349 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt @@ -47,6 +47,13 @@ internal data class LauncherApp( override val label: String = launcherActivityInfo.label.toString() + /** + * Cached result of the normalized label. + * First string is the normalizer ID + * Second string is the normalized label + */ + internal var cachedNormalizerResult: Pair? = null + constructor( context: Context, diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt index 98da0720a..f48e884b9 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.withContext @@ -169,7 +170,7 @@ internal class AppShortcutRepositoryImpl( } else { persistentListOf() } - } + }.flowOn(Dispatchers.Default) } private val shortcutChangeEmitter = callbackFlow { diff --git a/data/i18n/src/main/java/de/mm20/launcher2/data/CompatStringNormalizer.kt b/data/i18n/src/main/java/de/mm20/launcher2/data/CompatStringNormalizer.kt index bcad5fef4..b86d2ba3a 100644 --- a/data/i18n/src/main/java/de/mm20/launcher2/data/CompatStringNormalizer.kt +++ b/data/i18n/src/main/java/de/mm20/launcher2/data/CompatStringNormalizer.kt @@ -8,6 +8,9 @@ import java.util.Locale * Pre Android 10 StringNormalizer. Only strips accents from latin characters */ internal class CompatStringNormalizer: StringNormalizer { + + override val id: String = "null" + override fun normalize(input: String): String { return StringUtils.stripAccents(input.lowercase(Locale.getDefault())) .replace("æ", "ae") diff --git a/data/i18n/src/main/java/de/mm20/launcher2/data/IcuStringNormalizer.kt b/data/i18n/src/main/java/de/mm20/launcher2/data/IcuStringNormalizer.kt index 6b15c4cec..3e9073f58 100644 --- a/data/i18n/src/main/java/de/mm20/launcher2/data/IcuStringNormalizer.kt +++ b/data/i18n/src/main/java/de/mm20/launcher2/data/IcuStringNormalizer.kt @@ -22,21 +22,26 @@ internal class IcuStringNormalizer( localeSettings: LocaleSettings, ) : StringNormalizer { + override val id: String + get() = transliteratorId.value ?: "null" + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private val transliterator = localeSettings.transliterator + private val transliteratorId = localeSettings.transliterator .map { - try { - getTransliterator(it) - } catch (e: IllegalArgumentException) { - CrashReporter.logException(e) - null - } + getTransliteratorId(it) } .stateIn(scope, SharingStarted.Eagerly, null) override fun normalize(input: String): String { - val transliterator = transliterator.value + val id = transliteratorId.value + + val transliterator = try { + Transliterator.getInstance(id) + } catch (e: IllegalArgumentException) { + CrashReporter.logException(e) + null + } if (transliterator == null) { return StringUtils.stripAccents(input.lowercase(Locale.getDefault())) @@ -48,17 +53,17 @@ internal class IcuStringNormalizer( return transliterator.transliterate(input).lowercase() } - private fun getTransliterator(preferenceValue: String?): Transliterator { - val id = preferenceValue ?: return Transliterator.getInstance(DisabledTransliteratorId) + private fun getTransliteratorId(preferenceValue: String?): String { + val id = preferenceValue ?: return DisabledTransliteratorId if (id.isNotBlank()) { - return Transliterator.getInstance("$id;$BaseTransliteratorId") + return "$id;$BaseTransliteratorId" } val locales = context.resources.configuration.locales if (locales.isEmpty) { - Transliterator.getInstance(BaseTransliteratorId) + return BaseTransliteratorId } val scripts = mutableSetOf() @@ -79,7 +84,7 @@ internal class IcuStringNormalizer( val id = availableIds.find { it.startsWith(filter) } if (id != null) { - return Transliterator.getInstance("$id;$BaseTransliteratorId") + return "$id;$BaseTransliteratorId" } languages.add(lng) @@ -91,12 +96,12 @@ internal class IcuStringNormalizer( val id = availableIds.find { it.startsWith(filter) } if (id != null) { - return Transliterator.getInstance("$id;$BaseTransliteratorId") + return "$id;$BaseTransliteratorId" } scripts.add(ulocale.script) } } - return Transliterator.getInstance(BaseTransliteratorId) + return BaseTransliteratorId } companion object {