Annual refactor

This commit is contained in:
MM20
2023-10-28 14:20:16 +02:00
parent 4f64652aae
commit 957358c79a
110 changed files with 1786 additions and 1663 deletions
@@ -0,0 +1,11 @@
package de.mm20.launcher2.appshortcuts
import android.content.Context
import android.content.Intent
import de.mm20.launcher2.search.AppShortcut
fun AppShortcut(context: Context, pinRequestIntent: Intent): AppShortcut? {
return LauncherShortcut.fromPinRequestIntent(context, pinRequestIntent)
?: LegacyShortcut.fromPinRequestIntent(context, pinRequestIntent)
}
@@ -0,0 +1,38 @@
package de.mm20.launcher2.appshortcuts
import android.content.Context
import android.content.IntentSender
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.graphics.drawable.Drawable
import android.os.Process
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.romanize
import de.mm20.launcher2.search.AppProfile
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.text.Collator
class AppShortcutConfigActivity(
private val launcherActivityInfo: LauncherActivityInfo,
): Comparable<AppShortcutConfigActivity> {
val label = launcherActivityInfo.label.toString()
val profile: AppProfile = if (launcherActivityInfo.user == Process.myUserHandle()) AppProfile.Personal else AppProfile.Work
fun getIcon(context: Context): Flow<Drawable?> = flow {
val icon = launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi)
emit(icon)
}
fun getIntent(context: Context): IntentSender? {
val launcherApps = context.getSystemService<LauncherApps>()!!
return launcherApps.getShortcutConfigActivityIntent(launcherActivityInfo)
}
override fun compareTo(other: AppShortcutConfigActivity): Int {
val label1 = label
val label2 = other.label
return Collator.getInstance().apply { strength = Collator.SECONDARY }
.compare(label1.romanize(), label2.romanize())
}
}
@@ -1,21 +1,19 @@
package de.mm20.launcher2.appshortcuts
import android.content.ComponentName
import android.content.Context
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.os.UserHandle
import android.util.Log
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -28,22 +26,25 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.Locale
interface AppShortcutRepository {
interface AppShortcutRepository : SearchableRepository<AppShortcut> {
fun search(query: String): Flow<ImmutableList<AppShortcut>>
suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo,
count: Int = 5
): List<LauncherShortcut>
fun findMany(
componentName: ComponentName? = null,
user: UserHandle = Process.myUserHandle(),
manifest: Boolean = false,
dynamic: Boolean = false,
pinned: Boolean = false,
cached: Boolean = false,
limit: Int = 5,
): Flow<ImmutableList<AppShortcut>>
suspend fun getShortcutsConfigActivities(): List<LauncherApp>
fun removePinnedShortcut(shortcut: LauncherShortcut)
suspend fun getShortcutsConfigActivities(): List<AppShortcutConfigActivity>
}
internal class AppShortcutRepositoryImpl(
@@ -53,33 +54,58 @@ internal class AppShortcutRepositoryImpl(
private val scope = CoroutineScope(Dispatchers.Default + Job())
override suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo,
count: Int,
) = withContext(Dispatchers.IO) {
val launcherApps = context.getSystemService<LauncherApps>()!!
if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList()
val query = LauncherApps.ShortcutQuery()
.setPackage(launcherActivityInfo.applicationInfo.packageName)
.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST)
val shortcuts = try {
launcherApps.getShortcuts(query, launcherActivityInfo.user)
} catch (e: IllegalStateException) {
emptyList()
}
val appShortcuts = mutableListOf<LauncherShortcut>()
appShortcuts.addAll(shortcuts
?.let {
if (it.size > count) it.subList(0, count)
else it
}
?.map {
LauncherShortcut(
context,
it,
override fun findMany(
componentName: ComponentName?,
user: UserHandle,
manifest: Boolean,
dynamic: Boolean,
pinned: Boolean,
cached: Boolean,
limit: Int
): Flow<ImmutableList<AppShortcut>> = flow {
val shortcuts = withContext(Dispatchers.IO) {
val launcherApps = context.getSystemService<LauncherApps>()!!
if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList()
val query = LauncherApps.ShortcutQuery()
.setActivity(componentName)
.setQueryFlags(
buildQueryFlags(manifest, dynamic, pinned, cached)
)
} ?: emptyList())
appShortcuts
val shortcuts = try {
launcherApps.getShortcuts(query, user)
} catch (e: IllegalStateException) {
emptyList()
}
val appShortcuts = mutableListOf<LauncherShortcut>()
appShortcuts.addAll(shortcuts
?.let {
if (it.size > limit) it.subList(0, limit)
else it
}
?.map {
LauncherShortcut(
context,
it,
)
} ?: emptyList()
)
appShortcuts
}
emit(shortcuts.toImmutableList())
}
private fun buildQueryFlags(
manifest: Boolean,
dynamic: Boolean,
pinned: Boolean,
cached: Boolean,
): Int {
var flags = 0
if (manifest) flags = flags or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST
if (dynamic) flags = flags or LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC
if (pinned) flags = flags or LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED
if (cached) flags = flags or LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
return flags
}
override fun search(query: String) = channelFlow<ImmutableList<AppShortcut>> {
@@ -93,7 +119,6 @@ internal class AppShortcutRepositoryImpl(
return@withContext
}
shortcutChangeEmitter.collectLatest {
val launcherApps =
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
@@ -180,39 +205,16 @@ internal class AppShortcutRepositoryImpl(
}
}.shareIn(scope, SharingStarted.WhileSubscribed(500), 1)
override fun removePinnedShortcut(shortcut: LauncherShortcut) {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return
val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply {
setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
}
val userHandle = shortcut.launcherShortcut.userHandle
val allPinned = launcherApps.getShortcuts(pinnedShortcutsQuery, userHandle)
if (allPinned == null) {
Log.e("MM20", "Could not remove shortcut ${shortcut.key}: shortcut query returned null")
return
}
launcherApps.pinShortcuts(
shortcut.launcherShortcut.`package`,
allPinned.filter { it.id != shortcut.launcherShortcut.id }.map { it.id },
userHandle
)
}
override suspend fun getShortcutsConfigActivities(): List<LauncherApp> {
override suspend fun getShortcutsConfigActivities(): List<AppShortcutConfigActivity> {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return emptyList()
val results = mutableListOf<LauncherApp>()
val results = mutableListOf<AppShortcutConfigActivity>()
val profiles = launcherApps.profiles
for (profile in profiles) {
val activities = launcherApps.getShortcutConfigActivityList(null, profile)
results.addAll(
activities.map {
LauncherApp(
context, it
)
AppShortcutConfigActivity(it)
}
)
}
@@ -11,9 +11,6 @@ import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.data.LegacyShortcut
import de.mm20.launcher2.search.data.UnavailableShortcut
import org.json.JSONObject
import org.koin.core.component.KoinComponent
@@ -37,7 +34,7 @@ class LauncherShortcutDeserializer(
val context: Context
) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): SavableSearchable? {
override suspend fun deserialize(serialized: String): SavableSearchable? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val json = JSONObject(serialized)
@@ -65,16 +62,9 @@ class LauncherShortcutDeserializer(
} catch (e: IllegalStateException) {
return null
}
val pm = context.packageManager
val appName = try {
pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (shortcuts == null || shortcuts.isEmpty()) {
if (shortcuts.isNullOrEmpty()) {
return null
} else {
val activity = shortcuts[0].activity
return LauncherShortcut(
context = context,
launcherShortcut = shortcuts[0],
@@ -84,6 +74,21 @@ class LauncherShortcutDeserializer(
}
}
class UnavailableShortcutSerializer: SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String? {
searchable as UnavailableShortcut
return jsonObjectOf(
"packagename" to searchable.packageName,
"id" to searchable.shortcutId,
"user" to searchable.userSerial,
).toString()
}
override val typePrefix: String
get() = LauncherShortcut.Domain
}
class LegacyShortcutSerializer: SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as LegacyShortcut
@@ -106,7 +111,7 @@ class LegacyShortcutSerializer: SearchableSerializer {
class LegacyShortcutDeserializer(
val context: Context
): SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable {
override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized)
val label = json.getString("label")
val intent = Intent.parseUri(json.getString("intent"), 0)
@@ -1,6 +1,7 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.appshortcuts
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
@@ -12,11 +13,13 @@ import android.os.Process
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.lang.NullPointerException
@@ -24,7 +27,7 @@ import java.lang.NullPointerException
/**
* Represents a modern (Android O+) launcher shortcut
*/
data class LauncherShortcut(
internal data class LauncherShortcut(
val launcherShortcut: ShortcutInfo,
override val appName: String?,
internal val userSerialNumber: Long,
@@ -32,6 +35,11 @@ data class LauncherShortcut(
) : AppShortcut {
override val domain: String = Domain
override val componentName: ComponentName?
get() = launcherShortcut.activity
override val packageName: String
get() = launcherShortcut.`package`
constructor(
context: Context,
@@ -48,7 +56,7 @@ data class LauncherShortcut(
)
override val label: String
get() = launcherShortcut.shortLabel?.toString() ?: ""
get() = launcherShortcut.shortLabel?.toString() ?: launcherShortcut.longLabel?.toString() ?: ""
override fun overrideLabel(label: String): LauncherShortcut {
return this.copy(labelOverride = label)
@@ -56,8 +64,10 @@ data class LauncherShortcut(
override val preferDetailsOverLaunch: Boolean = false
private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
override val profile: AppProfile
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
override val key: String
get() = if (isMainProfile) {
@@ -145,6 +155,31 @@ data class LauncherShortcut(
)
}
override fun getSerializer(): SearchableSerializer {
return LauncherShortcutSerializer()
}
override suspend fun delete(context: Context) {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return
val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply {
setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
}
val userHandle = launcherShortcut.userHandle
val allPinned = launcherApps.getShortcuts(pinnedShortcutsQuery, userHandle)
if (allPinned == null) {
Log.e("MM20", "Could not remove shortcut ${key}: shortcut query returned null")
return
}
launcherApps.pinShortcuts(
launcherShortcut.`package`,
allPinned.filter { it.id != launcherShortcut.id }.map { it.id },
userHandle
)
}
companion object {
fun fromPinRequestIntent(context: Context, data: Intent): LauncherShortcut? {
val launcherApps =
@@ -1,5 +1,6 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.appshortcuts
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.Intent.ShortcutIconResource
@@ -10,8 +11,11 @@ import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getDrawableOrNull
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.SearchableSerializer
data class LegacyShortcut(
internal data class LegacyShortcut(
val intent: Intent,
override val label: String,
override val appName: String?,
@@ -22,6 +26,9 @@ data class LegacyShortcut(
override val domain = Domain
override val key: String = "$domain://${intent.toUri(0)}"
override val profile: AppProfile
get() = AppProfile.Personal
override fun overrideLabel(label: String): LegacyShortcut {
return this.copy(labelOverride = label)
}
@@ -31,7 +38,10 @@ data class LegacyShortcut(
return context.tryStartActivity(intent, options)
}
val packageName: String?
override val componentName: ComponentName?
get() = intent.component
override val packageName: String?
get() = intent.`package` ?: intent.component?.packageName
override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? {
@@ -75,6 +85,10 @@ data class LegacyShortcut(
)
}
override fun getSerializer(): SearchableSerializer {
return LegacyShortcutSerializer()
}
companion object {
const val Domain = "legacyshortcut"
@@ -1,8 +1,15 @@
package de.mm20.launcher2.appshortcuts
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableRepository
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module
val appShortcutsModule = module {
single<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get()) }
factory<SearchableRepository<AppShortcut>>(named<AppShortcut>()) { AppShortcutRepositoryImpl(androidContext(), get()) }
factory<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get()) }
factory<SearchableDeserializer>(named(LauncherShortcut.Domain)) { LauncherShortcutDeserializer(androidContext()) }
factory<SearchableDeserializer>(named(LegacyShortcut.Domain)) { LegacyShortcutDeserializer(androidContext()) }
}
@@ -1,24 +1,27 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.appshortcuts
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Process
import android.os.UserManager
import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableSerializer
/**
* Shortcut class that is used when a [LauncherShortcut] is not available, e.g. missing permissions
* when Kvaesitso is not set as default launcher.
*/
class UnavailableShortcut(
internal class UnavailableShortcut(
override val label: String,
override val appName: String?,
val packageName: String,
override val packageName: String,
val shortcutId: String,
val isMainProfile: Boolean,
val userSerial: Long,
@@ -34,6 +37,8 @@ class UnavailableShortcut(
override val labelOverride: String?
get() = null
override val componentName: ComponentName?
get() = null
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
@@ -56,6 +61,14 @@ class UnavailableShortcut(
return false
}
override fun getSerializer(): SearchableSerializer {
return UnavailableShortcutSerializer()
}
override val isUnavailable: Boolean = true
override val profile: AppProfile
get() = TODO("Not yet implemented")
companion object {
internal operator fun invoke(context: Context, id: String, packageName: String, userSerial: Long): UnavailableShortcut? {
val appInfo = try {
@@ -1,36 +0,0 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.SavableSearchable
interface AppShortcut: SavableSearchable {
val appName: String?
override val preferDetailsOverLaunch: Boolean
get() = false
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
color = 0xFF3DDA84.toInt(),
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
scale = 0.65f,
),
backgroundLayer = ColorLayer(0xFF3DDA84.toInt()),
)
}
companion object {
fun fromPinRequestIntent(context: Context, data: Intent): AppShortcut? {
return LauncherShortcut.fromPinRequestIntent(context, data)
?: LegacyShortcut.fromPinRequestIntent(context, data)
}
}
}