add setting to exclude apps from shortcuts search
This commit is contained in:
@@ -52,6 +52,8 @@ import de.mm20.launcher2.ui.settings.appearance.ImportThemeSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.appearance.ImportThemeSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.apps.AppSearchSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.apps.AppSearchSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.appshortcuts.AppShortcutsSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.appshortcuts.AppShortcutsSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.backup.BackupSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.breezyweather.BreezyWeatherSettingsRoute
|
||||
@@ -261,6 +263,9 @@ class SettingsActivity : BaseActivity() {
|
||||
entry<FavoritesSettingsRoute> {
|
||||
FavoritesSettingsScreen()
|
||||
}
|
||||
entry<AppShortcutsSettingsRoute> {
|
||||
AppShortcutsSettingsScreen()
|
||||
}
|
||||
entry<ContactsSettingsRoute> {
|
||||
ContactsSettingsScreen()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package de.mm20.launcher2.ui.settings.appshortcuts
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.applications.AppRepository
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.badges.BadgeService
|
||||
import de.mm20.launcher2.icons.IconService
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class AppShortcutSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
private val appRepository by inject<AppRepository>()
|
||||
private val settings by inject<ShortcutSearchSettings>()
|
||||
private val permissionsManager by inject<PermissionsManager>()
|
||||
private val iconService by inject<IconService>()
|
||||
private val badgeService by inject<BadgeService>()
|
||||
|
||||
val blocklist =
|
||||
settings.blocklist.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptySet())
|
||||
|
||||
val apps = appRepository.findMany().map { it.sorted() }.flowOn(Dispatchers.Default)
|
||||
|
||||
val isShortcutSearchEnabled = settings.enabled
|
||||
|
||||
val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
fun setShortcutSearchEnabled(enabled: Boolean) {
|
||||
settings.setEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setAppBlocklisted(appId: String, blocked: Boolean) {
|
||||
val blocklist = blocklist.value.toMutableSet()
|
||||
if (blocked) {
|
||||
blocklist.add(appId)
|
||||
} else {
|
||||
blocklist.remove(appId)
|
||||
}
|
||||
settings.setBlocklist(blocklist)
|
||||
}
|
||||
|
||||
fun requestAppShortcutsPermission(activity: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.AppShortcuts)
|
||||
}
|
||||
|
||||
fun getIcon(app: SavableSearchable, size: Int): Flow<LauncherIcon?> {
|
||||
return iconService.getIcon(app, size)
|
||||
}
|
||||
|
||||
fun getBadge(app: SavableSearchable): Flow<Badge?> {
|
||||
return badgeService.getBadge(app)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package de.mm20.launcher2.ui.settings.appshortcuts
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import de.mm20.launcher2.ktx.getSerialNumber
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
|
||||
import de.mm20.launcher2.ui.component.preferences.GuardedPreference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
|
||||
data object AppShortcutsSettingsRoute : NavKey
|
||||
|
||||
@Composable
|
||||
fun AppShortcutsSettingsScreen() {
|
||||
|
||||
val viewModel: AppShortcutSettingsScreenVM = viewModel()
|
||||
|
||||
val searchEnabled by viewModel.isShortcutSearchEnabled.collectAsStateWithLifecycle(null)
|
||||
val hasPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(
|
||||
null
|
||||
)
|
||||
val apps by viewModel.apps.collectAsStateWithLifecycle(emptyList())
|
||||
val blocklist by viewModel.blocklist.collectAsStateWithLifecycle()
|
||||
|
||||
val activity = LocalActivity.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val xsShape = MaterialTheme.shapes.extraSmall
|
||||
val mdShape = MaterialTheme.shapes.medium
|
||||
|
||||
PreferenceScreen(
|
||||
title = { Text(stringResource(R.string.preference_search_appshortcuts)) },
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
item {
|
||||
Box(modifier = Modifier.padding(bottom = 10.dp)) {
|
||||
|
||||
PreferenceCategory {
|
||||
GuardedPreference(
|
||||
locked = hasPermission == false,
|
||||
onUnlock = {
|
||||
viewModel.requestAppShortcutsPermission(activity as AppCompatActivity)
|
||||
},
|
||||
description = stringResource(
|
||||
R.string.missing_permission_appshortcuts_search_settings,
|
||||
stringResource(R.string.app_name),
|
||||
),
|
||||
) {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.preference_search_appshortcuts),
|
||||
summary = stringResource(R.string.preference_search_appshortcuts_summary),
|
||||
icon = R.drawable.mobile_arrow_up_right_24px,
|
||||
value = searchEnabled == true && hasPermission == true,
|
||||
onValueChanged = {
|
||||
viewModel.setShortcutSearchEnabled(it)
|
||||
},
|
||||
enabled = hasPermission == true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(apps) { index, app ->
|
||||
val key = "${app.componentName.packageName}:${app.user.getSerialNumber(context)}"
|
||||
Box(
|
||||
modifier = Modifier.clip(
|
||||
when {
|
||||
apps.size == 1 -> mdShape
|
||||
index == 0 -> mdShape.copy(
|
||||
bottomEnd = xsShape.bottomEnd,
|
||||
bottomStart = xsShape.bottomStart
|
||||
)
|
||||
|
||||
index == apps.size - 1 -> mdShape.copy(
|
||||
topEnd = xsShape.topEnd,
|
||||
topStart = xsShape.topStart
|
||||
)
|
||||
|
||||
else -> xsShape
|
||||
}
|
||||
)
|
||||
) {
|
||||
CheckboxPreference(
|
||||
icon = {
|
||||
val size = 32.dp
|
||||
val icon by viewModel.getIcon(app, size.toPixels().toInt())
|
||||
.collectAsStateWithLifecycle(null)
|
||||
val badge by viewModel.getBadge(app)
|
||||
.collectAsStateWithLifecycle(null)
|
||||
ShapedLauncherIcon(size = size, icon = { icon }, badge = { badge })
|
||||
},
|
||||
title = app.label,
|
||||
value = !blocklist.contains(key),
|
||||
onValueChanged = {
|
||||
viewModel.setAppBlocklisted(key, !it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||
import de.mm20.launcher2.ui.launcher.search.filters.SearchFilters
|
||||
import de.mm20.launcher2.ui.locals.LocalBackStack
|
||||
import de.mm20.launcher2.ui.settings.apps.AppSearchSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.appshortcuts.AppShortcutsSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarProviderSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.contacts.ContactsSettingsRoute
|
||||
@@ -208,15 +209,18 @@ fun SearchSettingsScreen() {
|
||||
stringResource(R.string.app_name),
|
||||
),
|
||||
) {
|
||||
SwitchPreference(
|
||||
PreferenceWithSwitch(
|
||||
title = stringResource(R.string.preference_search_appshortcuts),
|
||||
summary = stringResource(R.string.preference_search_appshortcuts_summary),
|
||||
icon = R.drawable.mobile_arrow_up_right_24px,
|
||||
value = appShortcuts == true && hasAppShortcutsPermission == true,
|
||||
onValueChanged = {
|
||||
switchValue = appShortcuts == true && hasAppShortcutsPermission == true,
|
||||
onSwitchChanged = {
|
||||
viewModel.setAppShortcuts(it)
|
||||
},
|
||||
enabled = hasAppShortcutsPermission == true
|
||||
enabled = hasAppShortcutsPermission == true,
|
||||
onClick = {
|
||||
backStack += AppShortcutsSettingsRoute
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ import android.os.Process
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
|
||||
private val cache = mutableMapOf<Int, Long>()
|
||||
|
||||
fun UserHandle.getSerialNumber(context: Context): Long {
|
||||
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
|
||||
return userManager.getSerialNumberForUser(this)
|
||||
return cache.getOrPut(hashCode()) {
|
||||
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
|
||||
userManager.getSerialNumberForUser(this)
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ data class LauncherSettingsData internal constructor(
|
||||
val calendarSearchExcludedCalendars: Set<String> = setOf(),
|
||||
|
||||
val shortcutSearchEnabled: Boolean = true,
|
||||
val shortcutSearchBlocklist: Set<String> = setOf(),
|
||||
|
||||
val calculatorEnabled: Boolean = true,
|
||||
|
||||
|
||||
@@ -10,9 +10,22 @@ class ShortcutSearchSettings internal constructor(
|
||||
val enabled
|
||||
get() = dataStore.data.map { it.shortcutSearchEnabled }.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Set of blocked packages that should not be shown in the search results.
|
||||
* Format: packageName:userId
|
||||
*/
|
||||
val blocklist
|
||||
get() = dataStore.data.map { it.shortcutSearchBlocklist }.distinctUntilChanged()
|
||||
|
||||
fun setEnabled(enabled: Boolean) {
|
||||
dataStore.update {
|
||||
it.copy(shortcutSearchEnabled = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBlocklist(blocklist: Set<String>) {
|
||||
dataStore.update {
|
||||
it.copy(shortcutSearchBlocklist = blocklist)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.os.Looper
|
||||
import android.os.Process
|
||||
import android.os.UserHandle
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.ktx.getSerialNumber
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
|
||||
@@ -23,9 +24,9 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
@@ -86,17 +87,18 @@ internal class AppShortcutRepositoryImpl(
|
||||
emptyList()
|
||||
}
|
||||
val appShortcuts = mutableListOf<LauncherShortcut>()
|
||||
appShortcuts.addAll(shortcuts
|
||||
?.let {
|
||||
if (it.size > limit) it.subList(0, limit)
|
||||
else it
|
||||
}
|
||||
?.map {
|
||||
LauncherShortcut(
|
||||
context,
|
||||
it,
|
||||
)
|
||||
} ?: emptyList()
|
||||
appShortcuts.addAll(
|
||||
shortcuts
|
||||
?.let {
|
||||
if (it.size > limit) it.subList(0, limit)
|
||||
else it
|
||||
}
|
||||
?.map {
|
||||
LauncherShortcut(
|
||||
context,
|
||||
it,
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
appShortcuts
|
||||
}
|
||||
@@ -153,12 +155,15 @@ internal class AppShortcutRepositoryImpl(
|
||||
|
||||
val callback = object : LauncherApps.Callback() {
|
||||
override fun onPackageRemoved(packageName: String?, user: UserHandle?) {
|
||||
trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onPackageAdded(packageName: String?, user: UserHandle?) {
|
||||
trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onPackageChanged(packageName: String?, user: UserHandle?) {
|
||||
trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onPackagesAvailable(
|
||||
@@ -166,6 +171,7 @@ internal class AppShortcutRepositoryImpl(
|
||||
user: UserHandle?,
|
||||
replacing: Boolean
|
||||
) {
|
||||
trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onPackagesUnavailable(
|
||||
@@ -173,6 +179,7 @@ internal class AppShortcutRepositoryImpl(
|
||||
user: UserHandle?,
|
||||
replacing: Boolean
|
||||
) {
|
||||
trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onShortcutsChanged(
|
||||
@@ -196,40 +203,41 @@ internal class AppShortcutRepositoryImpl(
|
||||
private val rawShortcuts: Flow<List<NormalizedShortcut>> = combine(
|
||||
listOf(
|
||||
settings.enabled,
|
||||
settings.blocklist,
|
||||
permissionsManager.hasPermission(PermissionGroup.AppShortcuts),
|
||||
shortcutChangeEmitter
|
||||
)
|
||||
) { it }
|
||||
.map { (enabled, perm, _) ->
|
||||
enabled as Boolean
|
||||
perm as Boolean
|
||||
) { (enabled, blocklist, perm, _) ->
|
||||
enabled as Boolean
|
||||
perm as Boolean
|
||||
blocklist as Set<String>
|
||||
|
||||
if (!enabled || !perm) return@map emptyList()
|
||||
if (!enabled || !perm) return@combine emptyList()
|
||||
|
||||
val launcherApps =
|
||||
context.getSystemService<LauncherApps>() ?: return@map emptyList()
|
||||
val launcherApps =
|
||||
context.getSystemService<LauncherApps>() ?: return@combine emptyList()
|
||||
|
||||
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 result = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle()) ?: emptyList()
|
||||
val normalized = result.map {
|
||||
NormalizedShortcut(
|
||||
info = it,
|
||||
normalizedLabels = listOfNotNull(
|
||||
it.longLabel?.toString()?.let { l -> stringNormalizer.normalize(l) },
|
||||
it.shortLabel?.toString()?.let { l -> stringNormalizer.normalize(l) },
|
||||
)
|
||||
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 result = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle()) ?: emptyList()
|
||||
val normalized = result.mapNotNull {
|
||||
if ("${it.`package`}:${it.userHandle.getSerialNumber(context)}" in blocklist) return@mapNotNull null
|
||||
NormalizedShortcut(
|
||||
info = it,
|
||||
normalizedLabels = listOfNotNull(
|
||||
it.longLabel?.toString()?.let { l -> stringNormalizer.normalize(l) },
|
||||
it.shortLabel?.toString()?.let { l -> stringNormalizer.normalize(l) },
|
||||
)
|
||||
}
|
||||
normalized
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
normalized
|
||||
}
|
||||
.shareIn(scope, SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
override suspend fun getShortcutsConfigActivities(): List<AppShortcutConfigActivity> {
|
||||
|
||||
@@ -17,7 +17,7 @@ kotlinx-coroutines = "1.11.0"
|
||||
kotlinx-immutable = "0.5.1"
|
||||
kotlinx-serialization = "1.11.0"
|
||||
|
||||
jetbrains-markdown = "0.7.8"
|
||||
jetbrains-markdown = "0.7.9"
|
||||
|
||||
androidx-compose = "1.13.0-alpha01"
|
||||
androidx-compose-material3 = "1.5.0-alpha26"
|
||||
@@ -33,7 +33,6 @@ androidx-palette = "1.0.0"
|
||||
androidx-room = "2.8.4"
|
||||
androidx-emojipicker = "1.6.0"
|
||||
|
||||
accompanist = "0.36.0"
|
||||
haze = "1.7.2"
|
||||
coil = "2.7.0"
|
||||
koin = "4.2.2"
|
||||
|
||||
Reference in New Issue
Block a user