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
+52
View File
@@ -0,0 +1,52 @@
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.applications"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(libs.commons.text)
implementation(project(":core:base"))
implementation(project(":core:ktx"))
implementation(project(":core:compat"))
}
+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.kts.kts.kts.kts.kts.kts.kts.kts.kts.
#
# 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 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
/
</manifest>
@@ -0,0 +1,186 @@
package de.mm20.launcher2.applications
import android.content.ComponentName
import android.content.Context
import android.content.Intent
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 de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.*
interface AppRepository {
fun getAllInstalledApps(): Flow<List<LauncherApp>>
fun getSuspendedPackages(): Flow<List<String>>
fun search(query: String): Flow<ImmutableList<LauncherApp>>
}
internal class AppRepositoryImpl(
private val context: Context,
) : AppRepository {
private val launcherApps =
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
private val installedApps = MutableStateFlow<List<LauncherApp>>(emptyList())
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
private val profiles: List<UserHandle> =
launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
init {
launcherApps.registerCallback(object : LauncherApps.Callback() {
override fun onPackagesUnavailable(
packageNames: Array<out String>,
user: UserHandle,
replacing: Boolean
) {
installedApps.value =
installedApps.value.filter { !packageNames.contains(it.`package`) }
}
override fun onPackageChanged(packageName: String, user: UserHandle) {
val apps = installedApps.value.toMutableList()
apps.removeAll { packageName == it.`package` }
apps.addAll(getApplications(packageName))
installedApps.value = apps
}
override fun onPackagesAvailable(
packageNames: Array<out String>,
user: UserHandle,
replacing: Boolean
) {
val apps = installedApps.value.toMutableList()
for (packageName in packageNames) {
apps.addAll(getApplications(packageName))
}
installedApps.value = apps
}
override fun onPackageAdded(packageName: String, user: UserHandle) {
Log.d("MM20", "App installed: $packageName")
val apps = installedApps.value.toMutableList()
apps.addAll(getApplications(packageName))
installedApps.value = apps
}
override fun onPackageRemoved(packageName: String, user: UserHandle) {
installedApps.value =
installedApps.value.filter { packageName != (it.`package`) || it.getUser() != user }
}
override fun onShortcutsChanged(
packageName: String,
shortcuts: MutableList<ShortcutInfo>,
user: UserHandle
) {
super.onShortcutsChanged(packageName, shortcuts, user)
onPackageChanged(packageName, user)
}
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
super.onPackagesSuspended(packageNames, user)
packageNames ?: return
suspendedPackages.value = suspendedPackages.value + packageNames
}
override fun onPackagesUnsuspended(
packageNames: Array<out String>?,
user: UserHandle?
) {
super.onPackagesUnsuspended(packageNames, user)
packageNames ?: return
suspendedPackages.value =
suspendedPackages.value.filter { packageNames.contains(it) }
}
}, Handler(Looper.getMainLooper()))
val apps = profiles.map { p ->
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) }
}.flatten()
installedApps.value = apps
}
override fun getSuspendedPackages(): Flow<List<String>> {
return suspendedPackages
}
private fun getApplications(packageName: String): List<LauncherApp> {
if (packageName == context.packageName) return emptyList()
return profiles.map { p ->
launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) }
}.flatten()
}
private fun getApplication(
launcherActivityInfo: LauncherActivityInfo,
profile: UserHandle
): LauncherApp? {
if (launcherActivityInfo.applicationInfo.packageName == context.packageName && !context.packageName.endsWith(
".debug"
)
) return null
return LauncherApp(context, launcherActivityInfo)
}
override fun search(query: String): Flow<ImmutableList<LauncherApp>> = channelFlow {
installedApps.collectLatest { apps ->
withContext(Dispatchers.Default) {
val appResults = mutableListOf<LauncherApp>()
if (query.isEmpty()) {
appResults.addAll(apps)
} else {
appResults.addAll(apps.filter {
matches(it.label, query)
})
val componentName = ComponentName.unflattenFromString(query)
getActivityByComponentName(componentName)?.let { appResults.add(it) }
}
appResults.sort()
send(appResults.toImmutableList())
}
}
}
override fun getAllInstalledApps(): Flow<List<LauncherApp>> {
return installedApps
}
private fun matches(label: String, query: String): Boolean {
val normalizedLabel = label.normalize()
val fuzzyScore = FuzzyScore(Locale.getDefault())
return fuzzyScore.fuzzyScore(label, query) >= query.length * 1.5 ||
fuzzyScore.fuzzyScore(normalizedLabel, query.normalize()) >= query.length * 1.5
}
private fun getActivityByComponentName(componentName: ComponentName?): LauncherApp? {
componentName ?: return null
val intent = Intent().setComponent(componentName)
val lai = launcherApps.resolveActivity(intent, Process.myUserHandle())
return lai?.let {
LauncherApp(context, lai)
}
}
}
@@ -0,0 +1,8 @@
package de.mm20.launcher2.applications
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val applicationsModule = module {
single<AppRepository> { AppRepositoryImpl(androidContext()) }
}
@@ -0,0 +1,43 @@
package de.mm20.launcher2.search.data
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.os.UserManager
import androidx.core.content.getSystemService
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import org.json.JSONObject
class LauncherAppSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as LauncherApp
val json = JSONObject()
json.put("package", searchable.`package`)
json.put("activity", searchable.activity)
json.put("user", searchable.userSerialNumber)
return json.toString()
}
override val typePrefix: String
get() = "app"
}
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable? {
val json = JSONObject(serialized)
val launcherApps = context.getSystemService<LauncherApps>()!!
val userManager = context.getSystemService<UserManager>()!!
val userSerial = json.optLong("user")
val user = userManager.getUserForSerialNumber(userSerial) ?: return null
val pkg = json.getString("package")
val intent = Intent().also {
it.component = ComponentName(pkg, json.getString("activity"))
}
val launcherActivityInfo = launcherApps.resolveActivity(intent, user) ?: return null
return LauncherApp(context, launcherActivityInfo)
}
}
@@ -0,0 +1,194 @@
package de.mm20.launcher2.search.data
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Bundle
import android.os.Process
import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import de.mm20.launcher2.applications.R
import de.mm20.launcher2.compat.PackageManagerCompat
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.SavableSearchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
data class LauncherApp(
val launcherActivityInfo: LauncherActivityInfo,
override val label: String,
val `package`: String,
val activity: String,
val flags: Int,
val version: String?,
internal val userSerialNumber: Long,
override val labelOverride: String? = null,
) : SavableSearchable {
constructor(context: Context, launcherActivityInfo: LauncherActivityInfo): this(
launcherActivityInfo,
label = launcherActivityInfo.label.toString(),
`package` = launcherActivityInfo.applicationInfo.packageName,
activity = launcherActivityInfo.name,
flags = launcherActivityInfo.applicationInfo.flags,
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
userSerialNumber = launcherActivityInfo.user.getSerialNumber(context)
)
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
override val domain: String = Domain
override val preferDetailsOverLaunch: Boolean = false
override fun overrideLabel(label: String): LauncherApp {
return this.copy(labelOverride = label)
}
override val key: String
get() = if (isMainProfile) "${domain}://$`package`:$activity" else "${domain}://$`package`:$activity:${userSerialNumber}"
fun getUser(): UserHandle? {
return launcherActivityInfo.user
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
scale = 0.5f,
color = 0xff3dda84.toInt(),
),
backgroundLayer = ColorLayer(0xff3dda84.toInt())
)
}
override suspend fun loadIcon(
context: Context,
size: Int,
themed: Boolean,
): LauncherIcon? {
try {
val icon =
withContext(Dispatchers.IO) {
launcherActivityInfo.getIcon(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,
)
} else {
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = icon,
scale = 1f,
),
backgroundLayer = TransparentLayer
)
}
} catch (e: PackageManager.NameNotFoundException) {
return null
}
}
override fun launch(context: Context, options: Bundle?): Boolean {
val launcherApps = context.getSystemService<LauncherApps>()!!
try {
launcherApps.startMainActivity(
ComponentName(`package`, activity),
launcherActivityInfo.user,
null,
options
)
} catch (e: SecurityException) {
return false
} catch (e: ActivityNotFoundException) {
return false
}
return true
}
fun getStoreDetails(context: Context): StoreLink? {
val pm = context.packageManager
return try {
val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`)
getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`)
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
companion object {
private fun getStoreLinkForInstaller(
installerPackage: String?,
packageName: String?
): StoreLink? {
if (packageName == null) return null
return when (installerPackage) {
"de.amazon.mShop.android", "com.amazon.venezia" -> {
StoreLink(
"Amazon App Shop",
"http://www.amazon.com/gp/mas/dl/android?p=${packageName}"
)
}
"com.android.vending" -> {
StoreLink(
"Google Play Store",
"https://play.google.com/store/apps/details?id=${packageName}"
)
}
"org.fdroid.fdroid", "com.aurora.adroid" -> {
StoreLink(
"F-Droid",
"https://f-droid.org/packages/${packageName}"
)
}
else -> null
}
}
fun getPackageVersionName(context: Context, packageName: String): String? {
return try {
context.packageManager.getPackageInfo(packageName, 0).versionName
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
const val Domain = "app"
}
}
data class StoreLink(
val label: String,
val url: String
)