Add support for legacy app shortcuts

This commit is contained in:
MM20
2022-09-18 23:12:26 +02:00
parent b068e4d6fd
commit 5fcb6ceb2c
12 changed files with 394 additions and 152 deletions
@@ -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<AppShortcut>
): List<LauncherShortcut>
suspend fun getShortcutsConfigActivities(): List<LauncherApp>
fun search(query: String): Flow<List<AppShortcut>>
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<AppShortcut>()
val appShortcuts = mutableListOf<LauncherShortcut>()
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 {
@@ -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,
)
}
}
@@ -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<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(
@@ -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
)
}
}
@@ -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()
)
}
}
}
@@ -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
)
}
}
}