From ab114595c4fddc8f49e8952f41b95b00c143bcb1 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat, 19 Mar 2022 17:41:04 +0100 Subject: [PATCH] Add app shortcut search --- .../appshortcuts/AppShortcutRepository.kt | 89 ++++++++++++++++--- .../launcher2/ui/launcher/search/SearchVM.kt | 8 ++ .../ui/launcher/search/SearchView.kt | 2 + .../search/appshortcuts/AppShortcutResults.kt | 29 ++++++ 4 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/search/appshortcuts/AppShortcutResults.kt diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt index 3f70afa1..166d8df5 100644 --- a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -3,26 +3,35 @@ package de.mm20.launcher2.appshortcuts import android.content.Context import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps -import android.content.pm.ShortcutInfo -import android.os.UserHandle +import android.content.pm.PackageManager +import android.os.Process import androidx.core.content.getSystemService +import com.github.promeg.pinyinhelper.Pinyin import de.mm20.launcher2.search.data.AppShortcut import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.withContext +import org.apache.commons.text.similarity.FuzzyScore +import java.util.* interface AppShortcutRepository { - suspend fun getShortcutsForActivity(launcherActivityInfo: LauncherActivityInfo, count: Int = 5): List - + suspend fun getShortcutsForActivity( + launcherActivityInfo: LauncherActivityInfo, + count: Int = 5 + ): List + fun search(query: String): Flow> } internal class AppShortcutRepositoryImpl( private val context: Context -): AppShortcutRepository { +) : AppShortcutRepository { + override suspend fun getShortcutsForActivity( launcherActivityInfo: LauncherActivityInfo, count: Int, - ) = withContext(Dispatchers.IO){ + ) = withContext(Dispatchers.IO) { val launcherApps = context.getSystemService()!! if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList() val query = LauncherApps.ShortcutQuery() @@ -40,12 +49,68 @@ internal class AppShortcutRepositoryImpl( else it } ?.map { - AppShortcut( - context, - it, - launcherActivityInfo.label.toString() - ) - } ?: emptyList()) + AppShortcut( + context, + it, + launcherActivityInfo.label.toString() + ) + } ?: emptyList()) appShortcuts } + + override fun search(query: String) = channelFlow> { + val launcherApps = context.getSystemService() ?: return@channelFlow send( + emptyList() + ) + + if (query.length < 3) { + return@channelFlow send(emptyList()) + } + + withContext(Dispatchers.IO) { + val shortcutQuery = LauncherApps.ShortcutQuery() + shortcutQuery.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or + LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or + LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or + LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED or + LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER) + val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle()) + ?.filter { + if (it.longLabel != null) { + return@filter matches(it.longLabel.toString(), query) + } + if (it.shortLabel != null) { + return@filter matches(it.shortLabel.toString(), query) + } + return@filter false + } ?: emptyList() + + val pm = context.packageManager + + send(shortcuts.map { + val label = try { + pm.getApplicationInfo(it.`package`, 0).loadLabel(pm).toString() + } catch (e: PackageManager.NameNotFoundException) { + "" + } + AppShortcut( + context, + it, + label + ) + }.sorted()) + } + } + + + private fun matches(label: String, query: String): Boolean { + val labelLatin = romanize(label) + val fuzzyScore = FuzzyScore(Locale.getDefault()) + return fuzzyScore.fuzzyScore(label, query) >= query.length * 1.5 || + fuzzyScore.fuzzyScore(labelLatin, query) >= query.length * 1.5 + } + + private fun romanize(label: String): String { + return Pinyin.toPinyin(label, "").lowercase(Locale.getDefault()) + } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index a7c46782..34564de3 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import de.mm20.launcher2.applications.AppRepository +import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.calculator.CalculatorRepository import de.mm20.launcher2.calendar.CalendarRepository import de.mm20.launcher2.contacts.ContactRepository @@ -36,6 +37,7 @@ class SearchVM : ViewModel(), KoinComponent { private val calendarRepository: CalendarRepository by inject() private val contactRepository: ContactRepository by inject() private val appRepository: AppRepository by inject() + private val appShortcutRepository: AppShortcutRepository by inject() private val wikipediaRepository: WikipediaRepository by inject() private val unitConverterRepository: UnitConverterRepository by inject() private val calculatorRepository: CalculatorRepository by inject() @@ -52,6 +54,7 @@ class SearchVM : ViewModel(), KoinComponent { } val appResults = MutableLiveData>(emptyList()) + val appShortcutResults = MutableLiveData>(emptyList()) val fileResults = MutableLiveData>(emptyList()) val contactResults = MutableLiveData>(emptyList()) val calendarResults = MutableLiveData>(emptyList()) @@ -124,6 +127,11 @@ class SearchVM : ViewModel(), KoinComponent { websearchResults.postValue(it) } } + jobs += async { + appShortcutRepository.search(query).collectLatest { + appShortcutResults.postValue(it) + } + } jobs.map { it.await() } isSearching.postValue(false) } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchView.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchView.kt index 751fdd69..7292025d 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchView.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp import de.mm20.launcher2.ui.MdcLauncherTheme import de.mm20.launcher2.ui.base.ProvideSettings import de.mm20.launcher2.ui.launcher.search.apps.AppResults +import de.mm20.launcher2.ui.launcher.search.appshortcuts.AppShortcutResults import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorResults import de.mm20.launcher2.ui.launcher.search.calendar.CalendarResults import de.mm20.launcher2.ui.launcher.search.contacts.ContactResults @@ -44,6 +45,7 @@ class SearchView @JvmOverloads constructor( ) { FavoritesResults() AppResults() + AppShortcutResults() UnitConverterResults() CalculatorResults() CalendarResults() diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/appshortcuts/AppShortcutResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/appshortcuts/AppShortcutResults.kt new file mode 100644 index 00000000..57b1ef0a --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/appshortcuts/AppShortcutResults.kt @@ -0,0 +1,29 @@ +package de.mm20.launcher2.ui.launcher.search.appshortcuts + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.ui.component.LauncherCard +import de.mm20.launcher2.ui.launcher.search.SearchVM +import de.mm20.launcher2.ui.launcher.search.common.SearchResultGrid + +@Composable +fun ColumnScope.AppShortcutResults() { + val viewModel: SearchVM = viewModel() + val apps by viewModel.appShortcutResults.observeAsState(emptyList()) + + AnimatedVisibility(apps.isNotEmpty()) { + LauncherCard( + modifier = Modifier.padding(bottom = 8.dp) + ) { + SearchResultGrid(items = apps) + } + } + +} \ No newline at end of file