Add app shortcut search
This commit is contained in:
parent
b73c9fabc9
commit
ab114595c4
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user