Reorganize and group modules

This commit is contained in:
MM20
2022-12-13 17:37:26 +01:00
parent bac24baad2
commit 3f8880a90a
995 changed files with 501 additions and 298 deletions
+1
View File
@@ -0,0 +1 @@
/build
+51
View File
@@ -0,0 +1,51 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
namespace = "de.mm20.launcher2.appshortcuts"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.koin.android)
implementation(libs.commons.text)
implementation(project(":data:applications"))
implementation(project(":core:permissions"))
implementation(project(":core:base"))
implementation(project(":core:ktx"))
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,229 @@
package de.mm20.launcher2.appshortcuts
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.Locale
interface AppShortcutRepository {
fun search(query: String): Flow<ImmutableList<AppShortcut>>
suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo,
count: Int = 5
): List<LauncherShortcut>
suspend fun getShortcutsConfigActivities(): List<LauncherApp>
fun removePinnedShortcut(shortcut: LauncherShortcut)
}
internal class AppShortcutRepositoryImpl(
private val context: Context,
private val permissionsManager: PermissionsManager,
) : AppShortcutRepository {
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,
)
} ?: emptyList())
appShortcuts
}
override fun search(query: String) = channelFlow<ImmutableList<AppShortcut>> {
if (query.length < 3) {
send(persistentListOf())
return@channelFlow
}
withContext(Dispatchers.IO) {
if (!permissionsManager.checkPermissionOnce(PermissionGroup.AppShortcuts)) {
send(persistentListOf())
return@withContext
}
shortcutChangeEmitter.collectLatest {
val launcherApps =
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
persistentListOf()
)
val shortcutQuery = LauncherApps.ShortcutQuery()
shortcutQuery.setQueryFlags(
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED or
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
)
val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle())
?.filter {
if (it.longLabel != null) {
return@filter matches(it.longLabel.toString(), query)
}
if (it.shortLabel != null) {
return@filter matches(it.shortLabel.toString(), query)
}
return@filter false
} ?: emptyList()
val pm = context.packageManager
send(
shortcuts.mapNotNull {
LauncherShortcut(
context,
it
)
}.toImmutableList()
)
}
}
}
private val shortcutChangeEmitter = callbackFlow {
send(Unit)
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val callback = object : LauncherApps.Callback() {
override fun onPackageRemoved(packageName: String?, user: UserHandle?) {
}
override fun onPackageAdded(packageName: String?, user: UserHandle?) {
}
override fun onPackageChanged(packageName: String?, user: UserHandle?) {
}
override fun onPackagesAvailable(
packageNames: Array<out String>?,
user: UserHandle?,
replacing: Boolean
) {
}
override fun onPackagesUnavailable(
packageNames: Array<out String>?,
user: UserHandle?,
replacing: Boolean
) {
}
override fun onShortcutsChanged(
packageName: String,
shortcuts: MutableList<ShortcutInfo>,
user: UserHandle
) {
super.onShortcutsChanged(packageName, shortcuts, user)
trySend(Unit)
}
}
launcherApps.registerCallback(callback, Handler(Looper.getMainLooper()))
awaitClose {
launcherApps.unregisterCallback(callback)
}
}.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> {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return emptyList()
val results = mutableListOf<LauncherApp>()
val profiles = launcherApps.profiles
for (profile in profiles) {
val activities = launcherApps.getShortcutConfigActivityList(null, profile)
results.addAll(
activities.map {
LauncherApp(
context, it
)
}
)
}
return results.sorted()
}
private fun matches(label: String, query: String): Boolean {
val labelLatin = label.normalize()
val fuzzyScore = FuzzyScore(Locale.getDefault())
return fuzzyScore.fuzzyScore(label, query) >= query.length * 1.5 ||
fuzzyScore.fuzzyScore(labelLatin, query.normalize()) >= query.length * 1.5
}
}
@@ -0,0 +1,138 @@
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.os.UserManager
import androidx.core.content.getSystemService
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 org.json.JSONObject
import org.koin.core.component.KoinComponent
class LauncherShortcutSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as LauncherShortcut
return jsonObjectOf(
"packagename" to searchable.launcherShortcut.`package`,
"id" to searchable.launcherShortcut.id,
"user" to searchable.userSerialNumber,
).toString()
}
override val typePrefix: String
get() = "shortcut"
}
class LauncherShortcutDeserializer(
val context: Context
) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): SavableSearchable? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return null
else {
val json = JSONObject(serialized)
val packageName = json.getString("packagename")
val id = json.getString("id")
val userSerial = json.optLong("user")
val query = LauncherApps.ShortcutQuery()
query.setPackage(packageName)
query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED or
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
)
query.setShortcutIds(mutableListOf(id))
val userManager = context.getSystemService<UserManager>()!!
val user = userManager.getUserForSerialNumber(userSerial) ?: return null
val shortcuts = try {
launcherApps.getShortcuts(query, user)
} 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()) {
return null
} else {
val activity = shortcuts[0].activity
return LauncherShortcut(
context = context,
launcherShortcut = shortcuts[0],
)
}
}
}
}
class LegacyShortcutSerializer: SearchableSerializer {
override fun serialize(searchable: SavableSearchable): 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): SavableSearchable {
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,
)
}
}
@@ -0,0 +1,8 @@
package de.mm20.launcher2.appshortcuts
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val appShortcutsModule = module {
single<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get()) }
}
@@ -0,0 +1,36 @@
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.5f,
),
backgroundLayer = ColorLayer(0xFF3DDA84.toInt()),
)
}
companion object {
fun fromPinRequestIntent(context: Context, data: Intent): AppShortcut? {
return LauncherShortcut.fromPinRequestIntent(context, data)
?: LegacyShortcut.fromPinRequestIntent(context, data)
}
}
}
@@ -0,0 +1,151 @@
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.PackageManager
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
*/
data class LauncherShortcut(
val launcherShortcut: ShortcutInfo,
override val appName: String?,
internal val userSerialNumber: Long,
override val labelOverride: String? = null,
) : AppShortcut {
override val domain: String = Domain
constructor(
context: Context,
launcherShortcut: ShortcutInfo,
): this(
launcherShortcut = launcherShortcut,
appName = try {
context.packageManager.getApplicationInfo(launcherShortcut.`package`, 0)
.loadLabel(context.packageManager).toString()
} catch (e: PackageManager.NameNotFoundException) {
null
},
userSerialNumber = launcherShortcut.userHandle.getSerialNumber(context)
)
override val label: String
get() = launcherShortcut.shortLabel?.toString() ?: ""
override fun overrideLabel(label: String): LauncherShortcut {
return this.copy(labelOverride = label)
}
override val preferDetailsOverLaunch: Boolean = false
val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
override val key: String
get() = if (isMainProfile) {
"$domain://${launcherShortcut.`package`}/${launcherShortcut.id}"
} else {
"$domain://${launcherShortcut.`package`}/${launcherShortcut.id}:${userSerialNumber}"
}
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,
)
}
const val Domain = "shortcut"
}
}
@@ -0,0 +1,105 @@
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.os.Bundle
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getDrawableOrNull
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
data class LegacyShortcut(
val intent: Intent,
override val label: String,
override val appName: String?,
val iconResource: ShortcutIconResource?,
override val labelOverride: String? = null,
) : AppShortcut {
override val domain = Domain
override val key: String = "$domain://${intent.toUri(0)}"
override fun overrideLabel(label: String): LegacyShortcut {
return this.copy(labelOverride = label)
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(intent, options)
}
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 {
const val Domain = "legacyshortcut"
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
)
}
}
}