Add support for legacy app shortcuts

This commit is contained in:
MM20 2022-09-18 23:12:26 +02:00
parent b068e4d6fd
commit 5fcb6ceb2c
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
12 changed files with 394 additions and 152 deletions

View File

@ -6,6 +6,7 @@ import android.content.pm.LauncherApps
import android.os.Bundle import android.os.Bundle
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherShortcut
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
class AddItemActivity : Activity() { class AddItemActivity : Activity() {
@ -14,15 +15,8 @@ class AddItemActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps val shortcut = AppShortcut.fromPinRequestIntent(this, intent)
val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() } if (shortcut != null) {
val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() }
val shortcut = AppShortcut(
this.applicationContext, shortcutInfo,
packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
.loadLabel(packageManager).toString()
)
if (pinRequest.accept()) {
favoritesRepository.pinItem(shortcut) favoritesRepository.pinItem(shortcut)
} }
finish() finish()

View File

@ -17,6 +17,7 @@ import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -30,13 +31,13 @@ interface AppShortcutRepository {
suspend fun getShortcutsForActivity( suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo, launcherActivityInfo: LauncherActivityInfo,
count: Int = 5 count: Int = 5
): List<AppShortcut> ): List<LauncherShortcut>
suspend fun getShortcutsConfigActivities(): List<LauncherApp> suspend fun getShortcutsConfigActivities(): List<LauncherApp>
fun search(query: String): Flow<List<AppShortcut>> fun search(query: String): Flow<List<AppShortcut>>
fun removePinnedShortcut(shortcut: AppShortcut) fun removePinnedShortcut(shortcut: LauncherShortcut)
} }
internal class AppShortcutRepositoryImpl( internal class AppShortcutRepositoryImpl(
@ -61,14 +62,14 @@ internal class AppShortcutRepositoryImpl(
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
emptyList() emptyList()
} }
val appShortcuts = mutableListOf<AppShortcut>() val appShortcuts = mutableListOf<LauncherShortcut>()
appShortcuts.addAll(shortcuts appShortcuts.addAll(shortcuts
?.let { ?.let {
if (it.size > count) it.subList(0, count) if (it.size > count) it.subList(0, count)
else it else it
} }
?.map { ?.map {
AppShortcut( LauncherShortcut(
context, context,
it, it,
launcherActivityInfo.label.toString() launcherActivityInfo.label.toString()
@ -128,7 +129,7 @@ internal class AppShortcutRepositoryImpl(
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
"" ""
} }
AppShortcut( LauncherShortcut(
context, context,
it, it,
label label
@ -186,7 +187,7 @@ internal class AppShortcutRepositoryImpl(
} }
}.shareIn(scope, SharingStarted.WhileSubscribed(500), 1) }.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 val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return if (!launcherApps.hasShortcutHostPermission()) return
val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply { val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply {

View File

@ -1,23 +1,27 @@
package de.mm20.launcher2.appshortcuts package de.mm20.launcher2.appshortcuts
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.Intent.ShortcutIconResource
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Process import android.os.Process
import android.os.UserManager import android.os.UserManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer 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 de.mm20.launcher2.search.data.Searchable
import org.json.JSONObject import org.json.JSONObject
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
class AppShortcutSerializer : SearchableSerializer { class LauncherShortcutSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: Searchable): String {
searchable as AppShortcut searchable as LauncherShortcut
return jsonObjectOf( return jsonObjectOf(
"packagename" to searchable.launcherShortcut.`package`, "packagename" to searchable.launcherShortcut.`package`,
"id" to searchable.launcherShortcut.id, "id" to searchable.launcherShortcut.id,
@ -30,7 +34,7 @@ class AppShortcutSerializer : SearchableSerializer {
} }
class AppShortcutDeserializer( class LauncherShortcutDeserializer(
val context: Context val context: Context
) : SearchableDeserializer, KoinComponent { ) : SearchableDeserializer, KoinComponent {
@ -68,7 +72,7 @@ class AppShortcutDeserializer(
return null return null
} else { } else {
val activity = shortcuts[0].activity val activity = shortcuts[0].activity
return AppShortcut( return LauncherShortcut(
context = context, context = context,
launcherShortcut = shortcuts[0], launcherShortcut = shortcuts[0],
appName = appName appName = appName
@ -77,3 +81,61 @@ 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,
)
}
}

View File

@ -1,60 +1,17 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.search.data
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent 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.ContextCompat
import androidx.core.content.getSystemService
import de.mm20.launcher2.appshortcuts.R import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.icons.TintedIconLayer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AppShortcut( abstract class AppShortcut(
context: Context, val appName: String?
val launcherShortcut: ShortcutInfo,
val appName: String
) : Searchable() { ) : 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<LauncherApps>()!!
try {
launcherApps.startShortcut(launcherShortcut, null, options)
} catch (e: IllegalStateException) {
return false
} catch (e: ActivityNotFoundException) {
return false
}
return true
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon( return StaticLauncherIcon(
foregroundLayer = TintedIconLayer( foregroundLayer = TintedIconLayer(
@ -66,49 +23,10 @@ class AppShortcut(
) )
} }
override suspend fun loadIcon( companion object {
context: Context, fun fromPinRequestIntent(context: Context, data: Intent): AppShortcut? {
size: Int, return LauncherShortcut.fromPinRequestIntent(context, data)
themed: Boolean, ?: LegacyShortcut.fromPinRequestIntent(context, data)
): 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
)
} }
} }

View File

@ -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<LauncherApps>()!!
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()
)
}
}
}

View File

@ -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
)
}
}
}

View File

@ -4,7 +4,8 @@ import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.graphics.BadgeDrawable 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 de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -15,7 +16,7 @@ class AppShortcutBadgeProvider(
private val context: Context private val context: Context
) : BadgeProvider { ) : BadgeProvider {
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow { override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is AppShortcut) { if (searchable is LauncherShortcut) {
val componentName = searchable.launcherShortcut.activity val componentName = searchable.launcherShortcut.activity
if (componentName == null) { if (componentName == null) {
send(null) send(null)
@ -32,6 +33,23 @@ class AppShortcutBadgeProvider(
val badge = Badge(icon = BadgeDrawable(context, icon)) val badge = Badge(icon = BadgeDrawable(context, icon))
send(badge) 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 { } else {
send(null) send(null)
} }

View File

@ -4,13 +4,14 @@ import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
class WorkProfileBadgeProvider : BadgeProvider { class WorkProfileBadgeProvider : BadgeProvider {
override fun getBadge(searchable: Searchable): Flow<Badge?> = flow { override fun getBadge(searchable: Searchable): Flow<Badge?> = flow {
if (searchable is LauncherApp && !searchable.isMainProfile || searchable is AppShortcut && !searchable.isMainProfile) { if (searchable is LauncherApp && !searchable.isMainProfile || searchable is LauncherShortcut && !searchable.isMainProfile) {
emit( emit(
Badge( Badge(
iconRes = R.drawable.ic_badge_workprofile iconRes = R.drawable.ic_badge_workprofile

View File

@ -1,8 +1,10 @@
package de.mm20.launcher2.favorites package de.mm20.launcher2.favorites
import android.content.Context import android.content.Context
import de.mm20.launcher2.appshortcuts.AppShortcutDeserializer import de.mm20.launcher2.appshortcuts.LauncherShortcutDeserializer
import de.mm20.launcher2.appshortcuts.AppShortcutSerializer 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.CalendarEventDeserializer
import de.mm20.launcher2.calendar.CalendarEventSerializer import de.mm20.launcher2.calendar.CalendarEventSerializer
import de.mm20.launcher2.contacts.ContactDeserializer import de.mm20.launcher2.contacts.ContactDeserializer
@ -23,8 +25,11 @@ internal fun getSerializer(searchable: Searchable?): SearchableSerializer {
if (searchable is LauncherApp) { if (searchable is LauncherApp) {
return LauncherAppSerializer() return LauncherAppSerializer()
} }
if (searchable is AppShortcut) { if (searchable is LauncherShortcut) {
return AppShortcutSerializer() return LauncherShortcutSerializer()
}
if (searchable is LegacyShortcut) {
return LegacyShortcutSerializer()
} }
if (searchable is CalendarEvent) { if (searchable is CalendarEvent) {
return CalendarEventSerializer() return CalendarEventSerializer()
@ -62,7 +67,10 @@ internal fun getDeserializer(context: Context, serialized: String): SearchableDe
return LauncherAppDeserializer(context) return LauncherAppDeserializer(context)
} }
if (type == "shortcut") { if (type == "shortcut") {
return AppShortcutDeserializer(context) return LauncherShortcutDeserializer(context)
}
if (type == "legacyshortcut") {
return LegacyShortcutDeserializer(context)
} }
if (type == "calendar") { if (type == "calendar") {
return CalendarEventDeserializer(context) return CalendarEventDeserializer(context)

View File

@ -2,13 +2,12 @@ package de.mm20.launcher2.ui.launcher.modals
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.ShortcutIconResource
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeRepository 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.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.AppShortcut 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.search.data.Searchable
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -164,6 +162,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
fun pickShortcut(section: FavoritesSheetSection) { fun pickShortcut(section: FavoritesSheetSection) {
createShortcutTarget.value = section createShortcutTarget.value = section
} }
fun cancelPickShortcut() { fun cancelPickShortcut() {
createShortcutTarget.value = null createShortcutTarget.value = null
} }
@ -180,24 +179,28 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
fun createShortcut(context: Context, data: Intent?) { fun createShortcut(context: Context, data: Intent?) {
data ?: return cancelPickShortcut() data ?: return cancelPickShortcut()
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val pinRequest = launcherApps.getPinItemRequest(data) ?: return cancelPickShortcut() val shortcut = AppShortcut.fromPinRequestIntent(context, data)
val shortcutInfo = pinRequest.shortcutInfo ?: return cancelPickShortcut()
pinRequest.accept() if (shortcut == null) {
val shortcut = AppShortcut( cancelPickShortcut()
context, return
shortcutInfo, }
context.packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
.loadLabel(context.packageManager).toString() if (!manuallySorted.any { it.key == shortcut.key }
) && !automaticallySorted.any { it.key == shortcut.key }
&& !frequentlyUsed.any { it.key == shortcut.key }
) {
if (createShortcutTarget.value == FavoritesSheetSection.ManuallySorted) { if (createShortcutTarget.value == FavoritesSheetSection.ManuallySorted) {
manuallySorted.add(shortcut) manuallySorted.add(shortcut)
} else { } else {
automaticallySorted.add(shortcut) automaticallySorted.add(shortcut)
} }
}
save() save()
buildItemList() buildItemList()
createShortcutTarget.value = null createShortcutTarget.value = null
} }
} }

View File

@ -4,7 +4,6 @@ package de.mm20.launcher2.ui.launcher.search.shortcut
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.* import androidx.compose.material3.*
@ -73,14 +72,16 @@ fun AppShortcutItem(
val textSpace by transition.animateDp(label = "textSpace") { val textSpace by transition.animateDp(label = "textSpace") {
if (it) 4.dp else 2.dp if (it) 4.dp else 2.dp
} }
shortcut.appName?.let {
Text( Text(
text = stringResource(R.string.shortcut_summary, shortcut.appName), text = stringResource(R.string.shortcut_summary, it),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = textSpace), modifier = Modifier.padding(top = textSpace),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
}
val badge by viewModel.badge.collectAsState(null) val badge by viewModel.badge.collectAsState(null)
val size by animateDpAsState(if (showDetails) 84.dp else 48.dp) val size by animateDpAsState(if (showDetails) 84.dp else 48.dp)
val iconSize = 84.dp.toPixels().toInt() val iconSize = 84.dp.toPixels().toInt()
@ -168,11 +169,14 @@ fun AppShortcutItem(
lifecycleOwner.lifecycleScope.launch { lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar( 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), actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short, duration = SnackbarDuration.Short,
) )
if(result == SnackbarResult.ActionPerformed) { if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide() viewModel.unhide()
} }
} }

View File

@ -8,6 +8,8 @@ import android.util.Log
import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.ktx.tryStartActivity 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.LegacyShortcut
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -16,12 +18,17 @@ class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(short
private val shortcutRepository: AppShortcutRepository by inject() private val shortcutRepository: AppShortcutRepository by inject()
val canDelete = shortcut.launcherShortcut.isPinned val canDelete = shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned
fun openAppInfo(context: Context) { fun openAppInfo(context: Context) {
val packageName = when(shortcut) {
is LegacyShortcut -> shortcut.intent.`package` ?: return
is LauncherShortcut -> shortcut.launcherShortcut.`package`
else -> return
}
context.tryStartActivity( context.tryStartActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { 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) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
) )
@ -29,7 +36,7 @@ class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(short
fun deleteShortcut() { fun deleteShortcut() {
if (!canDelete) return if (!canDelete) return
shortcutRepository.removePinnedShortcut(shortcut) if (shortcut is LauncherShortcut) shortcutRepository.removePinnedShortcut(shortcut)
favoritesRepository.unpinItem(shortcut) favoritesRepository.unpinItem(shortcut)
} }
} }