Add support for legacy app shortcuts
This commit is contained in:
@@ -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 {
|
||||
|
||||
+67
-5
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user