From d797263c0aa36c0774f1c9519f6f5c8625d91d8c Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:03:01 +0200 Subject: [PATCH] Don't unpin shortcuts when they are unavailable --- .../search/common/SearchableItemVM.kt | 7 ++ .../launcher/search/shortcut/ShortcutItem.kt | 13 +++- core/i18n/src/main/res/values/strings.xml | 3 + .../appshortcuts/AppShortcutSerialization.kt | 15 ++-- .../search/data/UnavailableShortcut.kt | 77 +++++++++++++++++++ .../providers/AppShortcutBadgeProvider.kt | 14 ++++ 6 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/UnavailableShortcut.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt index 34c3b3e8..8f887c4d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt @@ -18,6 +18,8 @@ import de.mm20.launcher2.files.FileRepository import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.notifications.Notification import de.mm20.launcher2.notifications.NotificationRepository +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.File @@ -45,6 +47,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { private val notificationRepository: NotificationRepository by inject() private val appShortcutRepository: AppShortcutRepository by inject() private val fileRepository: FileRepository by inject() + private val permissionsManager: PermissionsManager by inject() private val searchable = MutableStateFlow(null) private val iconSize = MutableStateFlow(0) @@ -154,4 +157,8 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { if (searchable is LauncherShortcut) appShortcutRepository.removePinnedShortcut(searchable) favoritesService.reset(searchable) } + + fun requestShortcutPermission(activity: AppCompatActivity) { + permissionsManager.requestPermission(activity, PermissionGroup.AppShortcuts) + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt index 84b2e688..92366f80 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt @@ -4,6 +4,7 @@ package de.mm20.launcher2.ui.launcher.search.shortcut import android.content.Intent import android.net.Uri import android.provider.Settings +import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.animateDp @@ -55,9 +56,11 @@ import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.LauncherShortcut import de.mm20.launcher2.search.data.LegacyShortcut +import de.mm20.launcher2.search.data.UnavailableShortcut import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.component.DefaultToolbarAction +import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.Toolbar import de.mm20.launcher2.ui.component.ToolbarAction @@ -98,6 +101,15 @@ fun AppShortcutItem( Column( modifier = modifier ) { + AnimatedVisibility(showDetails && shortcut is UnavailableShortcut) { + MissingPermissionBanner( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), + text = stringResource(R.string.shortcut_unavailable_description, stringResource(R.string.app_name)), + onClick = { + viewModel.requestShortcutPermission(context as AppCompatActivity) + } + ) + } Row { Column( modifier = Modifier @@ -154,7 +166,6 @@ fun AppShortcutItem( AnimatedVisibility(showDetails) { - val toolbarActions = mutableListOf() if (LocalFavoritesEnabled.current) { diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index e53efccf..87fe54d8 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -846,4 +846,7 @@ Restore default Apply theme The selected file could not be read. Please make sure that you selected a valid theme file (*.kvtheme), and that the file is not corrupt. + Unavailable + + This shortcut is unavailable because %1$s isn\'t the default launcher \ No newline at end of file diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt index 60d6bea5..43d6874d 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt @@ -13,6 +13,7 @@ import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.data.LauncherShortcut import de.mm20.launcher2.search.data.LegacyShortcut +import de.mm20.launcher2.search.data.UnavailableShortcut import org.json.JSONObject import org.koin.core.component.KoinComponent @@ -38,12 +39,16 @@ class LauncherShortcutDeserializer( override fun deserialize(serialized: String): SavableSearchable? { val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - if (!launcherApps.hasShortcutHostPermission()) return null + + val json = JSONObject(serialized) + val packageName = json.getString("packagename") + val id = json.getString("id") + val userSerial = json.optLong("user") + + if (!launcherApps.hasShortcutHostPermission()) { + return UnavailableShortcut(context, id, packageName, userSerial) + } else { - val json = JSONObject(serialized) - val packageName = json.getString("packagename") - val id = json.getString("id") - val userSerial = json.optLong("user") val query = LauncherApps.ShortcutQuery() query.setPackage(packageName) query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/UnavailableShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/UnavailableShortcut.kt new file mode 100644 index 00000000..c9b762aa --- /dev/null +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/UnavailableShortcut.kt @@ -0,0 +1,77 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Bundle +import android.os.Process +import android.os.UserManager +import de.mm20.launcher2.appshortcuts.R +import de.mm20.launcher2.icons.ColorLayer +import de.mm20.launcher2.icons.StaticLauncherIcon +import de.mm20.launcher2.icons.TintedIconLayer +import de.mm20.launcher2.search.SavableSearchable + +/** + * Shortcut class that is used when a [LauncherShortcut] is not available, e.g. missing permissions + * when Kvaesitso is not set as default launcher. + */ +class UnavailableShortcut( + override val label: String, + override val appName: String?, + val packageName: String, + val shortcutId: String, + val isMainProfile: Boolean, + val userSerial: Long, +): AppShortcut { + + + override val key: String + get() = if (isMainProfile) { + "$domain://${packageName}/${shortcutId}" + } else { + "$domain://${packageName}/${shortcutId}:userSerial" + } + + override val labelOverride: String? + get() = null + + override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { + return StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + icon = context.getDrawable(R.drawable.ic_file_android)!!, + color = 0xFF333333.toInt() + ), + backgroundLayer = ColorLayer(0xFF333333.toInt()), + ) + } + + override val domain: String + get() = LauncherShortcut.Domain + + override fun overrideLabel(label: String): SavableSearchable { + return this + } + + override fun launch(context: Context, options: Bundle?): Boolean { + return false + } + + companion object { + internal operator fun invoke(context: Context, id: String, packageName: String, userSerial: Long): UnavailableShortcut? { + val appInfo = try { + context.packageManager.getApplicationInfo(packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + return null + } + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + return UnavailableShortcut( + label = context.getString(R.string.shortcut_label_unavailable), + appName = appInfo.loadLabel(context.packageManager).toString(), + packageName = packageName, + shortcutId = id, + isMainProfile = userManager.getUserForSerialNumber(userSerial) == Process.myUserHandle(), + userSerial = userSerial, + ) + } + } +} \ No newline at end of file diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt index dba7bbc4..70f95874 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt @@ -7,6 +7,7 @@ import de.mm20.launcher2.graphics.BadgeDrawable import de.mm20.launcher2.search.data.LauncherShortcut import de.mm20.launcher2.search.data.LegacyShortcut import de.mm20.launcher2.search.Searchable +import de.mm20.launcher2.search.data.UnavailableShortcut import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow @@ -50,6 +51,19 @@ class AppShortcutBadgeProvider( val badge = Badge(icon = BadgeDrawable(context, icon)) send(badge) } + } else if (searchable is UnavailableShortcut) { + val packageName = searchable.packageName + withContext(Dispatchers.IO) { + val icon = try { + context.packageManager.getApplicationIcon( + packageName + ) + } catch (e: PackageManager.NameNotFoundException) { + return@withContext + } + val badge = Badge(icon = BadgeDrawable(context, icon)) + send(badge) + } } else { send(null) }