Add app shortcut search

This commit is contained in:
MM20 2022-03-19 17:41:04 +01:00
parent b73c9fabc9
commit ab114595c4
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
4 changed files with 116 additions and 12 deletions

View File

@ -3,26 +3,35 @@ package de.mm20.launcher2.appshortcuts
import android.content.Context import android.content.Context
import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo import android.content.pm.PackageManager
import android.os.UserHandle import android.os.Process
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.github.promeg.pinyinhelper.Pinyin
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.*
interface AppShortcutRepository { interface AppShortcutRepository {
suspend fun getShortcutsForActivity(launcherActivityInfo: LauncherActivityInfo, count: Int = 5): List<AppShortcut> suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo,
count: Int = 5
): List<AppShortcut>
fun search(query: String): Flow<List<AppShortcut>>
} }
internal class AppShortcutRepositoryImpl( internal class AppShortcutRepositoryImpl(
private val context: Context private val context: Context
): AppShortcutRepository { ) : AppShortcutRepository {
override suspend fun getShortcutsForActivity( override suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo, launcherActivityInfo: LauncherActivityInfo,
count: Int, count: Int,
) = withContext(Dispatchers.IO){ ) = withContext(Dispatchers.IO) {
val launcherApps = context.getSystemService<LauncherApps>()!! val launcherApps = context.getSystemService<LauncherApps>()!!
if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList() if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList()
val query = LauncherApps.ShortcutQuery() val query = LauncherApps.ShortcutQuery()
@ -40,12 +49,68 @@ internal class AppShortcutRepositoryImpl(
else it else it
} }
?.map { ?.map {
AppShortcut( AppShortcut(
context, context,
it, it,
launcherActivityInfo.label.toString() launcherActivityInfo.label.toString()
) )
} ?: emptyList()) } ?: emptyList())
appShortcuts appShortcuts
} }
override fun search(query: String) = channelFlow<List<AppShortcut>> {
val launcherApps = context.getSystemService<LauncherApps>() ?: 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())
}
} }

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.calculator.CalculatorRepository import de.mm20.launcher2.calculator.CalculatorRepository
import de.mm20.launcher2.calendar.CalendarRepository import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.contacts.ContactRepository import de.mm20.launcher2.contacts.ContactRepository
@ -36,6 +37,7 @@ class SearchVM : ViewModel(), KoinComponent {
private val calendarRepository: CalendarRepository by inject() private val calendarRepository: CalendarRepository by inject()
private val contactRepository: ContactRepository by inject() private val contactRepository: ContactRepository by inject()
private val appRepository: AppRepository by inject() private val appRepository: AppRepository by inject()
private val appShortcutRepository: AppShortcutRepository by inject()
private val wikipediaRepository: WikipediaRepository by inject() private val wikipediaRepository: WikipediaRepository by inject()
private val unitConverterRepository: UnitConverterRepository by inject() private val unitConverterRepository: UnitConverterRepository by inject()
private val calculatorRepository: CalculatorRepository by inject() private val calculatorRepository: CalculatorRepository by inject()
@ -52,6 +54,7 @@ class SearchVM : ViewModel(), KoinComponent {
} }
val appResults = MutableLiveData<List<Application>>(emptyList()) val appResults = MutableLiveData<List<Application>>(emptyList())
val appShortcutResults = MutableLiveData<List<AppShortcut>>(emptyList())
val fileResults = MutableLiveData<List<File>>(emptyList()) val fileResults = MutableLiveData<List<File>>(emptyList())
val contactResults = MutableLiveData<List<Contact>>(emptyList()) val contactResults = MutableLiveData<List<Contact>>(emptyList())
val calendarResults = MutableLiveData<List<CalendarEvent>>(emptyList()) val calendarResults = MutableLiveData<List<CalendarEvent>>(emptyList())
@ -124,6 +127,11 @@ class SearchVM : ViewModel(), KoinComponent {
websearchResults.postValue(it) websearchResults.postValue(it)
} }
} }
jobs += async {
appShortcutRepository.search(query).collectLatest {
appShortcutResults.postValue(it)
}
}
jobs.map { it.await() } jobs.map { it.await() }
isSearching.postValue(false) isSearching.postValue(false)
} }

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.MdcLauncherTheme import de.mm20.launcher2.ui.MdcLauncherTheme
import de.mm20.launcher2.ui.base.ProvideSettings import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.launcher.search.apps.AppResults 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.calculator.CalculatorResults
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarResults import de.mm20.launcher2.ui.launcher.search.calendar.CalendarResults
import de.mm20.launcher2.ui.launcher.search.contacts.ContactResults import de.mm20.launcher2.ui.launcher.search.contacts.ContactResults
@ -44,6 +45,7 @@ class SearchView @JvmOverloads constructor(
) { ) {
FavoritesResults() FavoritesResults()
AppResults() AppResults()
AppShortcutResults()
UnitConverterResults() UnitConverterResults()
CalculatorResults() CalculatorResults()
CalendarResults() CalendarResults()

View File

@ -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)
}
}
}