Don't unpin shortcuts when they are unavailable
This commit is contained in:
parent
ad5772b3db
commit
d797263c0a
@ -18,6 +18,8 @@ import de.mm20.launcher2.files.FileRepository
|
|||||||
import de.mm20.launcher2.icons.IconService
|
import de.mm20.launcher2.icons.IconService
|
||||||
import de.mm20.launcher2.notifications.Notification
|
import de.mm20.launcher2.notifications.Notification
|
||||||
import de.mm20.launcher2.notifications.NotificationRepository
|
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.SavableSearchable
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.File
|
import de.mm20.launcher2.search.data.File
|
||||||
@ -45,6 +47,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
|||||||
private val notificationRepository: NotificationRepository by inject()
|
private val notificationRepository: NotificationRepository by inject()
|
||||||
private val appShortcutRepository: AppShortcutRepository by inject()
|
private val appShortcutRepository: AppShortcutRepository by inject()
|
||||||
private val fileRepository: FileRepository by inject()
|
private val fileRepository: FileRepository by inject()
|
||||||
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
|
|
||||||
private val searchable = MutableStateFlow<SavableSearchable?>(null)
|
private val searchable = MutableStateFlow<SavableSearchable?>(null)
|
||||||
private val iconSize = MutableStateFlow<Int>(0)
|
private val iconSize = MutableStateFlow<Int>(0)
|
||||||
@ -154,4 +157,8 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
|||||||
if (searchable is LauncherShortcut) appShortcutRepository.removePinnedShortcut(searchable)
|
if (searchable is LauncherShortcut) appShortcutRepository.removePinnedShortcut(searchable)
|
||||||
favoritesService.reset(searchable)
|
favoritesService.reset(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestShortcutPermission(activity: AppCompatActivity) {
|
||||||
|
permissionsManager.requestPermission(activity, PermissionGroup.AppShortcuts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ package de.mm20.launcher2.ui.launcher.search.shortcut
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.MutableTransitionState
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
import androidx.compose.animation.core.animateDp
|
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.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
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.R
|
||||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
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.ShapedLauncherIcon
|
||||||
import de.mm20.launcher2.ui.component.Toolbar
|
import de.mm20.launcher2.ui.component.Toolbar
|
||||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||||
@ -98,6 +101,15 @@ fun AppShortcutItem(
|
|||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
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 {
|
Row {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -154,7 +166,6 @@ fun AppShortcutItem(
|
|||||||
|
|
||||||
|
|
||||||
AnimatedVisibility(showDetails) {
|
AnimatedVisibility(showDetails) {
|
||||||
|
|
||||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||||
|
|
||||||
if (LocalFavoritesEnabled.current) {
|
if (LocalFavoritesEnabled.current) {
|
||||||
|
|||||||
@ -846,4 +846,7 @@
|
|||||||
<string name="preference_restore_default">Restore default</string>
|
<string name="preference_restore_default">Restore default</string>
|
||||||
<string name="import_theme_apply">Apply theme</string>
|
<string name="import_theme_apply">Apply theme</string>
|
||||||
<string name="import_theme_error">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.</string>
|
<string name="import_theme_error">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.</string>
|
||||||
|
<string name="shortcut_label_unavailable">Unavailable</string>
|
||||||
|
<!-- %1$s: app name -->
|
||||||
|
<string name="shortcut_unavailable_description">This shortcut is unavailable because %1$s isn\'t the default launcher</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -13,6 +13,7 @@ import de.mm20.launcher2.search.SearchableDeserializer
|
|||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
import de.mm20.launcher2.search.data.LegacyShortcut
|
||||||
|
import de.mm20.launcher2.search.data.UnavailableShortcut
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
@ -38,12 +39,16 @@ class LauncherShortcutDeserializer(
|
|||||||
|
|
||||||
override fun deserialize(serialized: String): SavableSearchable? {
|
override fun deserialize(serialized: String): SavableSearchable? {
|
||||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
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 {
|
else {
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val packageName = json.getString("packagename")
|
|
||||||
val id = json.getString("id")
|
|
||||||
val userSerial = json.optLong("user")
|
|
||||||
val query = LauncherApps.ShortcutQuery()
|
val query = LauncherApps.ShortcutQuery()
|
||||||
query.setPackage(packageName)
|
query.setPackage(packageName)
|
||||||
query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or
|
query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or
|
||||||
|
|||||||
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import de.mm20.launcher2.graphics.BadgeDrawable
|
|||||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
import de.mm20.launcher2.search.data.LegacyShortcut
|
||||||
import de.mm20.launcher2.search.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
import de.mm20.launcher2.search.data.UnavailableShortcut
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
@ -50,6 +51,19 @@ class AppShortcutBadgeProvider(
|
|||||||
val badge = Badge(icon = BadgeDrawable(context, icon))
|
val badge = Badge(icon = BadgeDrawable(context, icon))
|
||||||
send(badge)
|
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 {
|
} else {
|
||||||
send(null)
|
send(null)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user