diff --git a/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt b/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt index 15f7e542..d8920a77 100644 --- a/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt +++ b/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt @@ -6,6 +6,7 @@ import android.content.pm.LauncherApps import android.os.Bundle import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.search.data.AppShortcut +import de.mm20.launcher2.search.data.LauncherShortcut import org.koin.android.ext.android.inject class AddItemActivity : Activity() { @@ -14,15 +15,8 @@ class AddItemActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() } - val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() } - val shortcut = AppShortcut( - this.applicationContext, shortcutInfo, - packageManager.getApplicationInfo(shortcutInfo.`package`, 0) - .loadLabel(packageManager).toString() - ) - if (pinRequest.accept()) { + val shortcut = AppShortcut.fromPinRequestIntent(this, intent) + if (shortcut != null) { favoritesRepository.pinItem(shortcut) } finish() diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt index 127650ca..b5c205b0 100644 --- a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -17,6 +17,7 @@ import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.data.LauncherShortcut import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -30,13 +31,13 @@ interface AppShortcutRepository { suspend fun getShortcutsForActivity( launcherActivityInfo: LauncherActivityInfo, count: Int = 5 - ): List + ): List suspend fun getShortcutsConfigActivities(): List fun search(query: String): Flow> - fun removePinnedShortcut(shortcut: AppShortcut) + fun removePinnedShortcut(shortcut: LauncherShortcut) } internal class AppShortcutRepositoryImpl( @@ -61,14 +62,14 @@ internal class AppShortcutRepositoryImpl( } catch (e: IllegalStateException) { emptyList() } - val appShortcuts = mutableListOf() + val appShortcuts = mutableListOf() appShortcuts.addAll(shortcuts ?.let { if (it.size > count) it.subList(0, count) else it } ?.map { - AppShortcut( + LauncherShortcut( context, it, launcherActivityInfo.label.toString() @@ -128,7 +129,7 @@ internal class AppShortcutRepositoryImpl( } catch (e: PackageManager.NameNotFoundException) { "" } - AppShortcut( + LauncherShortcut( context, it, label @@ -186,7 +187,7 @@ internal class AppShortcutRepositoryImpl( } }.shareIn(scope, SharingStarted.WhileSubscribed(500), 1) - override fun removePinnedShortcut(shortcut: AppShortcut) { + override fun removePinnedShortcut(shortcut: LauncherShortcut) { val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps if (!launcherApps.hasShortcutHostPermission()) return val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply { diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt index a41fc2ef..3685fc5d 100644 --- a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt @@ -1,23 +1,27 @@ package de.mm20.launcher2.appshortcuts import android.content.Context +import android.content.Intent +import android.content.Intent.ShortcutIconResource import android.content.pm.LauncherApps import android.content.pm.PackageManager +import android.net.Uri import android.os.Process import android.os.UserManager import androidx.core.content.getSystemService import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableSerializer -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.Searchable import org.json.JSONObject import org.koin.core.component.KoinComponent -class AppShortcutSerializer : SearchableSerializer { +class LauncherShortcutSerializer : SearchableSerializer { override fun serialize(searchable: Searchable): String { - searchable as AppShortcut + searchable as LauncherShortcut return jsonObjectOf( "packagename" to searchable.launcherShortcut.`package`, "id" to searchable.launcherShortcut.id, @@ -30,7 +34,7 @@ class AppShortcutSerializer : SearchableSerializer { } -class AppShortcutDeserializer( +class LauncherShortcutDeserializer( val context: Context ) : SearchableDeserializer, KoinComponent { @@ -68,7 +72,7 @@ class AppShortcutDeserializer( return null } else { val activity = shortcuts[0].activity - return AppShortcut( + return LauncherShortcut( context = context, launcherShortcut = shortcuts[0], appName = appName @@ -76,4 +80,62 @@ class AppShortcutDeserializer( } } } +} + +class LegacyShortcutSerializer: SearchableSerializer { + override fun serialize(searchable: Searchable): String? { + searchable as LegacyShortcut + return jsonObjectOf( + "label" to searchable.label, + "intent" to searchable.intent.toUri(0), + "iconResource" to searchable.iconResource?.let { + jsonObjectOf( + "package" to it.packageName, + "resource" to it.resourceName, + ) + } + ).toString() + } + + override val typePrefix: String + get() = "legacyshortcut" +} + +class LegacyShortcutDeserializer( + val context: Context +): SearchableDeserializer { + override fun deserialize(serialized: String): Searchable? { + val json = JSONObject(serialized) + val label = json.getString("label") + val intent = Intent.parseUri(json.getString("intent"), 0) + val iconResourceObj = json.optJSONObject("iconResource") + val iconResource = iconResourceObj?.let { + ShortcutIconResource().apply { + packageName = iconResourceObj.getString("package") + resourceName = iconResourceObj.getString("resource") + } + } + + val packageName = intent.`package` ?: intent.component?.packageName + + val appName = try { + packageName?.let { + context + .packageManager + .getApplicationInfo(it, 0) + .loadLabel(context.packageManager) + .toString() + } + } catch (e: PackageManager.NameNotFoundException) { + null + } + + return LegacyShortcut( + intent = intent, + label = label, + iconResource = iconResource, + appName = appName, + ) + } + } \ No newline at end of file diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt b/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt index 5abde3da..3ac2ee42 100644 --- a/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt +++ b/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt @@ -1,60 +1,17 @@ package de.mm20.launcher2.search.data -import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent -import android.content.pm.LauncherApps -import android.content.pm.ShortcutInfo -import android.graphics.Color -import android.graphics.drawable.AdaptiveIconDrawable -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import android.os.Process import androidx.core.content.ContextCompat -import androidx.core.content.getSystemService import de.mm20.launcher2.appshortcuts.R -import de.mm20.launcher2.icons.* -import de.mm20.launcher2.ktx.getSerialNumber -import de.mm20.launcher2.ktx.isAtLeastApiLevel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import de.mm20.launcher2.icons.ColorLayer +import de.mm20.launcher2.icons.StaticLauncherIcon +import de.mm20.launcher2.icons.TintedIconLayer -class AppShortcut( - context: Context, - val launcherShortcut: ShortcutInfo, - val appName: String +abstract class AppShortcut( + val appName: String? ) : Searchable() { - override val label: String - get() = launcherShortcut.shortLabel?.toString() ?: "" - - - internal val userSerialNumber: Long = launcherShortcut.userHandle.getSerialNumber(context) - val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle() - - override val key: String - get() = if (isMainProfile) { - "shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}" - } else { - "shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}:${userSerialNumber}" - } - - override fun getLaunchIntent(context: Context): Intent? { - return launcherShortcut.intent - } - - override fun launch(context: Context, options: Bundle?): Boolean { - val launcherApps = context.getSystemService()!! - try { - launcherApps.startShortcut(launcherShortcut, null, options) - } catch (e: IllegalStateException) { - return false - } catch (e: ActivityNotFoundException) { - return false - } - return true - } - override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { return StaticLauncherIcon( foregroundLayer = TintedIconLayer( @@ -66,49 +23,10 @@ class AppShortcut( ) } - override suspend fun loadIcon( - context: Context, - size: Int, - themed: Boolean, - ): LauncherIcon? { - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val icon = withContext(Dispatchers.IO) { - launcherApps.getShortcutIconDrawable( - launcherShortcut, - context.resources.displayMetrics.densityDpi - ) - } ?: return null - if (icon is AdaptiveIconDrawable) { - if (themed && isAtLeastApiLevel(33) && icon.monochrome != null) { - return StaticLauncherIcon( - foregroundLayer = TintedIconLayer( - scale = 1f, - icon = icon.monochrome!!, - ), - backgroundLayer = ColorLayer() - ) - } - return StaticLauncherIcon( - foregroundLayer = icon.foreground?.let { - StaticIconLayer( - icon = it, - scale = 1.5f, - ) - } ?: TransparentLayer, - backgroundLayer = icon.background?.let { - StaticIconLayer( - icon = it, - scale = 1.5f, - ) - } ?: TransparentLayer, - ) + companion object { + fun fromPinRequestIntent(context: Context, data: Intent): AppShortcut? { + return LauncherShortcut.fromPinRequestIntent(context, data) + ?: LegacyShortcut.fromPinRequestIntent(context, data) } - return StaticLauncherIcon( - foregroundLayer = StaticIconLayer( - icon = icon, - scale = 1f - ), - backgroundLayer = TransparentLayer - ) } } \ No newline at end of file diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LauncherShortcut.kt b/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LauncherShortcut.kt new file mode 100644 index 00000000..d1fb223d --- /dev/null +++ b/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LauncherShortcut.kt @@ -0,0 +1,132 @@ +package de.mm20.launcher2.search.data + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.ShortcutInfo +import android.graphics.drawable.AdaptiveIconDrawable +import android.os.Bundle +import android.os.Process +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import de.mm20.launcher2.appshortcuts.R +import de.mm20.launcher2.icons.* +import de.mm20.launcher2.ktx.getSerialNumber +import de.mm20.launcher2.ktx.isAtLeastApiLevel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Represents a modern (Android O+) launcher shortcut + */ +class LauncherShortcut( + context: Context, + val launcherShortcut: ShortcutInfo, + appName: String +) : AppShortcut(appName) { + + override val label: String + get() = launcherShortcut.shortLabel?.toString() ?: "" + + + internal val userSerialNumber: Long = launcherShortcut.userHandle.getSerialNumber(context) + val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle() + + override val key: String + get() = if (isMainProfile) { + "shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}" + } else { + "shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}:${userSerialNumber}" + } + + override fun getLaunchIntent(context: Context): Intent? { + return launcherShortcut.intent + } + + override fun launch(context: Context, options: Bundle?): Boolean { + val launcherApps = context.getSystemService()!! + try { + launcherApps.startShortcut(launcherShortcut, null, options) + } catch (e: IllegalStateException) { + return false + } catch (e: ActivityNotFoundException) { + return false + } + return true + } + + override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { + return StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + color = 0xFF3DDA84.toInt(), + icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!, + scale = 0.5f, + ), + backgroundLayer = ColorLayer(0xFF3DDA84.toInt()), + ) + } + + override suspend fun loadIcon( + context: Context, + size: Int, + themed: Boolean, + ): LauncherIcon? { + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val icon = withContext(Dispatchers.IO) { + launcherApps.getShortcutIconDrawable( + launcherShortcut, + context.resources.displayMetrics.densityDpi + ) + } ?: return null + if (icon is AdaptiveIconDrawable) { + if (themed && isAtLeastApiLevel(33) && icon.monochrome != null) { + return StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + scale = 1f, + icon = icon.monochrome!!, + ), + backgroundLayer = ColorLayer() + ) + } + return StaticLauncherIcon( + foregroundLayer = icon.foreground?.let { + StaticIconLayer( + icon = it, + scale = 1.5f, + ) + } ?: TransparentLayer, + backgroundLayer = icon.background?.let { + StaticIconLayer( + icon = it, + scale = 1.5f, + ) + } ?: TransparentLayer, + ) + } + return StaticLauncherIcon( + foregroundLayer = StaticIconLayer( + icon = icon, + scale = 1f + ), + backgroundLayer = TransparentLayer + ) + } + + companion object { + fun fromPinRequestIntent(context: Context, data: Intent): LauncherShortcut? { + val launcherApps = + context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val pinRequest = launcherApps.getPinItemRequest(data) + val shortcutInfo = pinRequest?.shortcutInfo ?: return null + if (!pinRequest.accept()) return null + return LauncherShortcut( + context, + shortcutInfo, + context.packageManager.getApplicationInfo(shortcutInfo.`package`, 0) + .loadLabel(context.packageManager).toString() + ) + } + } + +} \ No newline at end of file diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LegacyShortcut.kt b/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LegacyShortcut.kt new file mode 100644 index 00000000..bb318455 --- /dev/null +++ b/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LegacyShortcut.kt @@ -0,0 +1,94 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import android.content.Intent +import android.content.Intent.ShortcutIconResource +import android.graphics.drawable.AdaptiveIconDrawable +import android.util.Log +import de.mm20.launcher2.icons.* +import de.mm20.launcher2.ktx.getDrawableOrNull +import de.mm20.launcher2.ktx.isAtLeastApiLevel + +class LegacyShortcut( + val intent: Intent, + override val label: String, + appName: String?, + val iconResource: ShortcutIconResource?, +) : AppShortcut(appName) { + override val key: String + get() = "legacyshortcut://${intent.toUri(0)}" + + override fun getLaunchIntent(context: Context): Intent { + return intent + } + + val packageName: String? + get() = intent.`package` ?: intent.component?.packageName + + override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? { + if (iconResource == null) return null + val resources = context.packageManager.getResourcesForApplication(iconResource.packageName) + val drawableId = + resources.getIdentifier(iconResource.resourceName, "drawable", iconResource.packageName) + if (drawableId == 0) return null + val icon = resources.getDrawableOrNull(drawableId) ?: return null + if (icon is AdaptiveIconDrawable) { + if (themed && isAtLeastApiLevel(33) && icon.monochrome != null) { + return StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + scale = 1f, + icon = icon.monochrome!!, + ), + backgroundLayer = ColorLayer() + ) + } + return StaticLauncherIcon( + foregroundLayer = icon.foreground?.let { + StaticIconLayer( + icon = it, + scale = 1.5f, + ) + } ?: TransparentLayer, + backgroundLayer = icon.background?.let { + StaticIconLayer( + icon = it, + scale = 1.5f, + ) + } ?: TransparentLayer, + ) + } + return StaticLauncherIcon( + foregroundLayer = StaticIconLayer( + icon = icon, + scale = 1f + ), + backgroundLayer = TransparentLayer + ) + } + + companion object { + fun fromPinRequestIntent(context: Context, data: Intent): LegacyShortcut? { + val intent: Intent? = data.extras?.getParcelable(Intent.EXTRA_SHORTCUT_INTENT) + val name: String? = data.extras?.getString(Intent.EXTRA_SHORTCUT_NAME) + val iconResource: ShortcutIconResource? = + data.extras?.getParcelable(Intent.EXTRA_SHORTCUT_ICON_RESOURCE) + + if (intent == null || name == null) { + return null + } + + val packageName = intent.`package` ?: intent.component?.packageName + + return LegacyShortcut( + intent = intent, + appName = packageName?.let { + context.packageManager.getApplicationInfo( + it, 0 + ).loadLabel(context.packageManager).toString() + }, + label = name, + iconResource = iconResource + ) + } + } +} \ No newline at end of file diff --git a/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt b/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt index 91299864..5d35a81c 100644 --- a/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt +++ b/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt @@ -4,7 +4,8 @@ import android.content.Context import android.content.pm.PackageManager import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.graphics.BadgeDrawable -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.Searchable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -15,7 +16,7 @@ class AppShortcutBadgeProvider( private val context: Context ) : BadgeProvider { override fun getBadge(searchable: Searchable): Flow = channelFlow { - if (searchable is AppShortcut) { + if (searchable is LauncherShortcut) { val componentName = searchable.launcherShortcut.activity if (componentName == null) { send(null) @@ -32,6 +33,23 @@ class AppShortcutBadgeProvider( val badge = Badge(icon = BadgeDrawable(context, icon)) send(badge) } + } else if (searchable is LegacyShortcut) { + val packageName = searchable.packageName + if (packageName == null) { + send(null) + return@channelFlow + } + 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) } diff --git a/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt b/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt index 593eb91e..b5160208 100644 --- a/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt +++ b/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt @@ -4,13 +4,14 @@ import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.R import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.data.LauncherShortcut import de.mm20.launcher2.search.data.Searchable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class WorkProfileBadgeProvider : BadgeProvider { override fun getBadge(searchable: Searchable): Flow = flow { - if (searchable is LauncherApp && !searchable.isMainProfile || searchable is AppShortcut && !searchable.isMainProfile) { + if (searchable is LauncherApp && !searchable.isMainProfile || searchable is LauncherShortcut && !searchable.isMainProfile) { emit( Badge( iconRes = R.drawable.ic_badge_workprofile diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/Serialization.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/Serialization.kt index 09cd476f..bf7c26b1 100644 --- a/favorites/src/main/java/de/mm20/launcher2/favorites/Serialization.kt +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/Serialization.kt @@ -1,8 +1,10 @@ package de.mm20.launcher2.favorites import android.content.Context -import de.mm20.launcher2.appshortcuts.AppShortcutDeserializer -import de.mm20.launcher2.appshortcuts.AppShortcutSerializer +import de.mm20.launcher2.appshortcuts.LauncherShortcutDeserializer +import de.mm20.launcher2.appshortcuts.LauncherShortcutSerializer +import de.mm20.launcher2.appshortcuts.LegacyShortcutDeserializer +import de.mm20.launcher2.appshortcuts.LegacyShortcutSerializer import de.mm20.launcher2.calendar.CalendarEventDeserializer import de.mm20.launcher2.calendar.CalendarEventSerializer import de.mm20.launcher2.contacts.ContactDeserializer @@ -23,8 +25,11 @@ internal fun getSerializer(searchable: Searchable?): SearchableSerializer { if (searchable is LauncherApp) { return LauncherAppSerializer() } - if (searchable is AppShortcut) { - return AppShortcutSerializer() + if (searchable is LauncherShortcut) { + return LauncherShortcutSerializer() + } + if (searchable is LegacyShortcut) { + return LegacyShortcutSerializer() } if (searchable is CalendarEvent) { return CalendarEventSerializer() @@ -62,7 +67,10 @@ internal fun getDeserializer(context: Context, serialized: String): SearchableDe return LauncherAppDeserializer(context) } if (type == "shortcut") { - return AppShortcutDeserializer(context) + return LauncherShortcutDeserializer(context) + } + if (type == "legacyshortcut") { + return LegacyShortcutDeserializer(context) } if (type == "calendar") { return CalendarEventDeserializer(context) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesSheetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesSheetVM.kt index ee9c8f54..91d6d1c5 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesSheetVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesSheetVM.kt @@ -2,13 +2,12 @@ package de.mm20.launcher2.ui.launcher.modals import android.content.Context import android.content.Intent +import android.content.Intent.ShortcutIconResource import android.content.pm.LauncherApps -import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.lazy.grid.LazyGridItemInfo import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.BadgeRepository @@ -18,13 +17,12 @@ import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.search.data.AppShortcut -import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.data.LauncherShortcut +import de.mm20.launcher2.search.data.LegacyShortcut import de.mm20.launcher2.search.data.Searchable -import de.mm20.launcher2.ui.R import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -164,6 +162,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent { fun pickShortcut(section: FavoritesSheetSection) { createShortcutTarget.value = section } + fun cancelPickShortcut() { createShortcutTarget.value = null } @@ -180,24 +179,28 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent { fun createShortcut(context: Context, data: Intent?) { data ?: return cancelPickShortcut() - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val pinRequest = launcherApps.getPinItemRequest(data) ?: return cancelPickShortcut() - val shortcutInfo = pinRequest.shortcutInfo ?: return cancelPickShortcut() - pinRequest.accept() - val shortcut = AppShortcut( - context, - shortcutInfo, - context.packageManager.getApplicationInfo(shortcutInfo.`package`, 0) - .loadLabel(context.packageManager).toString() - ) - if (createShortcutTarget.value == FavoritesSheetSection.ManuallySorted) { - manuallySorted.add(shortcut) - } else { - automaticallySorted.add(shortcut) + + val shortcut = AppShortcut.fromPinRequestIntent(context, data) + + if (shortcut == null) { + cancelPickShortcut() + return + } + + if (!manuallySorted.any { it.key == shortcut.key } + && !automaticallySorted.any { it.key == shortcut.key } + && !frequentlyUsed.any { it.key == shortcut.key } + ) { + if (createShortcutTarget.value == FavoritesSheetSection.ManuallySorted) { + manuallySorted.add(shortcut) + } else { + automaticallySorted.add(shortcut) + } } save() buildItemList() createShortcutTarget.value = null } + } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt index 8b761061..399585f5 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt @@ -4,7 +4,6 @@ package de.mm20.launcher2.ui.launcher.search.shortcut import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.layout.* -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.* import androidx.compose.material3.* @@ -73,13 +72,15 @@ fun AppShortcutItem( val textSpace by transition.animateDp(label = "textSpace") { if (it) 4.dp else 2.dp } - Text( - text = stringResource(R.string.shortcut_summary, shortcut.appName), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = textSpace), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + shortcut.appName?.let { + Text( + text = stringResource(R.string.shortcut_summary, it), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = textSpace), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } } val badge by viewModel.badge.collectAsState(null) val size by animateDpAsState(if (showDetails) 84.dp else 48.dp) @@ -168,11 +169,14 @@ fun AppShortcutItem( lifecycleOwner.lifecycleScope.launch { val result = snackbarHostState.showSnackbar( - message = context.getString(R.string.msg_item_hidden, shortcut.label), + message = context.getString( + R.string.msg_item_hidden, + shortcut.label + ), actionLabel = context.getString(R.string.action_undo), duration = SnackbarDuration.Short, - ) - if(result == SnackbarResult.ActionPerformed) { + ) + if (result == SnackbarResult.ActionPerformed) { viewModel.unhide() } } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt index 600c0261..39aa8334 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt @@ -8,6 +8,8 @@ import android.util.Log import de.mm20.launcher2.appshortcuts.AppShortcutRepository 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.ui.launcher.search.common.SearchableItemVM import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -16,12 +18,17 @@ class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(short private val shortcutRepository: AppShortcutRepository by inject() - val canDelete = shortcut.launcherShortcut.isPinned + val canDelete = shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned fun openAppInfo(context: Context) { + val packageName = when(shortcut) { + is LegacyShortcut -> shortcut.intent.`package` ?: return + is LauncherShortcut -> shortcut.launcherShortcut.`package` + else -> return + } context.tryStartActivity( Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.parse("package:${shortcut.launcherShortcut.`package`}") + data = Uri.parse("package:$packageName") addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } ) @@ -29,7 +36,7 @@ class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(short fun deleteShortcut() { if (!canDelete) return - shortcutRepository.removePinnedShortcut(shortcut) + if (shortcut is LauncherShortcut) shortcutRepository.removePinnedShortcut(shortcut) favoritesRepository.unpinItem(shortcut) } } \ No newline at end of file