From 17f8906f26b6831cace347ea33446b494ce011cd Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:54:25 +0200 Subject: [PATCH] add setting to exclude apps from shortcuts search --- .../launcher2/ui/settings/SettingsActivity.kt | 5 + .../AppShortcutSettingsScreenVM.kt | 66 ++++++++++ .../AppShortcutsSettingsScreen.kt | 121 ++++++++++++++++++ .../settings/search/SearchSettingsScreen.kt | 12 +- .../java/de/mm20/launcher2/ktx/UserHandle.kt | 8 +- .../preferences/LauncherSettingsData.kt | 1 + .../search/ShortcutSearchSettings.kt | 13 ++ .../appshortcuts/AppShortcutRepository.kt | 84 ++++++------ gradle/libs.versions.toml | 3 +- 9 files changed, 267 insertions(+), 46 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/appshortcuts/AppShortcutSettingsScreenVM.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/appshortcuts/AppShortcutsSettingsScreen.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index ce96e221c..9cd0e904d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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 { FavoritesSettingsScreen() } + entry { + AppShortcutsSettingsScreen() + } entry { ContactsSettingsScreen() } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appshortcuts/AppShortcutSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appshortcuts/AppShortcutSettingsScreenVM.kt new file mode 100644 index 000000000..d75e9c6d3 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appshortcuts/AppShortcutSettingsScreenVM.kt @@ -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() + private val settings by inject() + private val permissionsManager by inject() + private val iconService by inject() + private val badgeService by inject() + + 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 { + return iconService.getIcon(app, size) + } + + fun getBadge(app: SavableSearchable): Flow { + return badgeService.getBadge(app) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appshortcuts/AppShortcutsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appshortcuts/AppShortcutsSettingsScreen.kt new file mode 100644 index 000000000..6696b6472 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appshortcuts/AppShortcutsSettingsScreen.kt @@ -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) + }, + ) + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt index 3b25b0fc8..6da660205 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt @@ -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 + } ) } diff --git a/core/ktx/src/main/java/de/mm20/launcher2/ktx/UserHandle.kt b/core/ktx/src/main/java/de/mm20/launcher2/ktx/UserHandle.kt index c798300dc..b56485b09 100644 --- a/core/ktx/src/main/java/de/mm20/launcher2/ktx/UserHandle.kt +++ b/core/ktx/src/main/java/de/mm20/launcher2/ktx/UserHandle.kt @@ -5,7 +5,11 @@ import android.os.Process import android.os.UserHandle import android.os.UserManager +private val cache = mutableMapOf() + 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) + } } \ No newline at end of file diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index 0956dfc36..fa6398e35 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -87,6 +87,7 @@ data class LauncherSettingsData internal constructor( val calendarSearchExcludedCalendars: Set = setOf(), val shortcutSearchEnabled: Boolean = true, + val shortcutSearchBlocklist: Set = setOf(), val calculatorEnabled: Boolean = true, diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/ShortcutSearchSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/ShortcutSearchSettings.kt index 044da5059..475cc980a 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/ShortcutSearchSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/ShortcutSearchSettings.kt @@ -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) { + dataStore.update { + it.copy(shortcutSearchBlocklist = blocklist) + } + } } \ No newline at end of file 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 7c4e61d12..6cfa3d3d2 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 @@ -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() - 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> = 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 - if (!enabled || !perm) return@map emptyList() + if (!enabled || !perm) return@combine emptyList() - val launcherApps = - context.getSystemService() ?: return@map emptyList() + val launcherApps = + context.getSystemService() ?: 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 { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4b5ebe009..61db3ff6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"