Add support for legacy app shortcuts
This commit is contained in:
parent
b068e4d6fd
commit
5fcb6ceb2c
@ -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()
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user