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,58 @@
package de.mm20.launcher2.applications
import android.content.ComponentName
import android.content.Context
import android.os.Bundle
import android.os.Process
import android.os.UserHandle
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.NullSerializer
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableSerializer
class FakeApp: Application {
override val componentName: ComponentName = ComponentName(randomString(), randomString())
override val isSystemApp: Boolean = false
override val isSuspended: Boolean = false
override val profile: AppProfile = AppProfile.Personal
override val user: UserHandle = Process.myUserHandle()
override val versionName: String = "1.0"
override val canUninstall: Boolean = false
override fun uninstall(context: Context) {
}
override fun openAppDetails(context: Context) {
}
override val canShareApk: Boolean = false
override val key: String = "fake://${randomString()}"
override val label: String = randomString()
override fun overrideLabel(label: String): SavableSearchable {
return this
}
override fun launch(context: Context, options: Bundle?): Boolean {
return false
}
override val domain: String
get() = "fake"
override fun getSerializer(): SearchableSerializer {
return NullSerializer()
}
private companion object {
private fun randomString(): String {
val charset = "abcdefghijklmnopqrstuvwxyz"
return (1..10)
.map { charset.random() }
.joinToString("")
}
}
}
@@ -6,8 +6,8 @@ import android.content.Intent
import android.content.pm.LauncherApps
import android.os.Process
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -16,51 +16,18 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
/**
* A fake implementation of [AppRepository] to simulate many installed apps.
* App repository that returns a fixed number of fake apps to simulate a large number of apps.
*/
class FakeAppRepository(private val context: Context, private val fakePackages: Int) : AppRepository {
class FakeAppRepository(private val context: Context, private val fakePackages: Int) : SearchableRepository<Application> {
private val fakeApp: LauncherApp
init {
val launcherApps = context.getSystemService<LauncherApps>()!!
fakeApp = LauncherApp(
context,
launcherApps.resolveActivity(
Intent().apply {
component = ComponentName(
context.packageName,
"de.mm20.launcher2.ui.launcher.LauncherActivity"
)
},
Process.myUserHandle()
),
)
}
private fun randomString(): String {
val charset = "abcdefghijklmnopqrstuvwxyz"
return (1..10)
.map { charset.random() }
.joinToString("")
}
override fun getAllInstalledApps(): Flow<List<LauncherApp>> {
return flowOf(buildList {
repeat(fakePackages) {
add(fakeApp.copy(`package` = randomString(), activity = randomString()))
}
})
}
override fun getSuspendedPackages(): Flow<List<String>> {
return flowOf(emptyList())
}
override fun search(query: String): Flow<ImmutableList<LauncherApp>> {
override fun search(query: String): Flow<ImmutableList<Application>> {
return if (query.isEmpty()) {
getAllInstalledApps().map { it.toImmutableList() }
buildList {
repeat(fakePackages) {
add(FakeApp())
}
}.toImmutableList().let { flowOf(it) }
} else {
flowOf(persistentListOf())
}
@@ -10,28 +10,34 @@ 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 de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.Locale
interface AppRepository {
fun getAllInstalledApps(): Flow<List<LauncherApp>>
fun getSuspendedPackages(): Flow<List<String>>
fun search(query: String): Flow<ImmutableList<LauncherApp>>
interface AppRepository : SearchableRepository<Application> {
override fun search(query: String): Flow<ImmutableList<Application>>
fun findMany(): Flow<ImmutableList<Application>>
}
internal class AppRepositoryImpl(
private val context: Context,
) : AppRepository {
private val scope = CoroutineScope(Dispatchers.Default + Job())
private val launcherApps =
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
@@ -40,9 +46,10 @@ internal class AppRepositoryImpl(
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
private val profiles: List<UserHandle> =
launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
private val profiles: List<UserHandle>
get() = launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
private val mutex = Mutex()
init {
launcherApps.registerCallback(object : LauncherApps.Callback() {
@@ -51,15 +58,23 @@ internal class AppRepositoryImpl(
user: UserHandle,
replacing: Boolean
) {
installedApps.value =
installedApps.value.filter { !packageNames.contains(it.`package`) }
scope.launch {
mutex.withLock {
installedApps.value =
installedApps.value.filter { !packageNames.contains(it.componentName.packageName) }
}
}
}
override fun onPackageChanged(packageName: String, user: UserHandle) {
val apps = installedApps.value.toMutableList()
apps.removeAll { packageName == it.`package` }
apps.addAll(getApplications(packageName))
installedApps.value = apps
scope.launch {
mutex.withLock {
val apps = installedApps.value.toMutableList()
apps.removeAll { packageName == it.componentName.packageName }
apps.addAll(getApplications(packageName))
installedApps.value = apps
}
}
}
override fun onPackagesAvailable(
@@ -67,23 +82,35 @@ internal class AppRepositoryImpl(
user: UserHandle,
replacing: Boolean
) {
val apps = installedApps.value.toMutableList()
for (packageName in packageNames) {
apps.addAll(getApplications(packageName))
scope.launch {
mutex.withLock {
val apps = installedApps.value.toMutableList()
for (packageName in packageNames) {
apps.addAll(getApplications(packageName))
}
installedApps.value = apps
}
}
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
scope.launch {
mutex.withLock {
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 }
scope.launch {
mutex.withLock {
installedApps.value =
installedApps.value.filter { packageName != (it.componentName.packageName) || it.user != user }
}
}
}
override fun onShortcutsChanged(
@@ -91,40 +118,55 @@ internal class AppRepositoryImpl(
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
scope.launch {
mutex.withLock {
installedApps.value = installedApps.value.map {
if (packageNames.contains(it.componentName.packageName)) {
it.copy(isSuspended = true)
} else {
it
}
}
}
}
}
override fun onPackagesUnsuspended(
packageNames: Array<out String>?,
user: UserHandle?
) {
super.onPackagesUnsuspended(packageNames, user)
packageNames ?: return
suspendedPackages.value =
suspendedPackages.value.filter { packageNames.contains(it) }
scope.launch {
mutex.withLock {
installedApps.value = installedApps.value.map {
if (packageNames.contains(it.componentName.packageName)) {
it.copy(isSuspended = false)
} else {
it
}
}
}
}
}
}, Handler(Looper.getMainLooper()))
val apps = profiles.map { p ->
try {
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) }
} catch (e: SecurityException) {
emptyList()
scope.launch {
mutex.withLock {
val apps = profiles.map { p ->
try {
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) }
} catch (e: SecurityException) {
emptyList()
}
}.flatten()
installedApps.value = apps
}
}.flatten()
installedApps.value = apps
}
override fun getSuspendedPackages(): Flow<List<String>> {
return suspendedPackages
}
}
private fun getApplications(packageName: String): List<LauncherApp> {
@@ -151,6 +193,10 @@ internal class AppRepositoryImpl(
return LauncherApp(context, launcherActivityInfo)
}
override fun findMany(): Flow<ImmutableList<Application>> {
return installedApps.map { it.toImmutableList() }
}
override fun search(query: String): Flow<ImmutableList<LauncherApp>> {
return installedApps.map { apps ->
withContext(Dispatchers.Default) {
@@ -171,10 +217,6 @@ internal class AppRepositoryImpl(
}
}
override fun getAllInstalledApps(): Flow<List<LauncherApp>> {
return installedApps
}
private fun matches(label: String, query: String): Boolean {
val normalizedLabel = label.normalize()
val normalizedQuery = query.normalize()
@@ -1,4 +1,4 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.applications
import android.content.ComponentName
import android.content.Context
@@ -15,8 +15,8 @@ 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("package", searchable.componentName.packageName)
json.put("activity", searchable.componentName.className)
json.put("user", searchable.userSerialNumber)
return json.toString()
}
@@ -26,7 +26,7 @@ class LauncherAppSerializer : SearchableSerializer {
}
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable? {
override suspend fun deserialize(serialized: String): SavableSearchable? {
val json = JSONObject(serialized)
val launcherApps = context.getSystemService<LauncherApps>()!!
val userManager = context.getSystemService<UserManager>()!!
@@ -1,9 +1,10 @@
package de.mm20.launcher2.search.data
package de.mm20.launcher2.applications
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
@@ -13,43 +14,56 @@ import android.net.Uri
import android.os.Bundle
import android.os.Process
import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
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.icons.ColorLayer
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.icons.TransparentLayer
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.StoreLink
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 data class LauncherApp(
private val launcherActivityInfo: LauncherActivityInfo,
override val versionName: String?,
override val isSuspended: Boolean = false,
internal val userSerialNumber: Long,
override val labelOverride: String? = null,
) : SavableSearchable {
) : Application {
constructor(context: Context, launcherActivityInfo: LauncherActivityInfo): this(
override val componentName: ComponentName
get() = launcherActivityInfo.componentName
override val label: String = launcherActivityInfo.label.toString()
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),
versionName = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
userSerialNumber = launcherActivityInfo.user.getSerialNumber(context)
)
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
override val user: UserHandle
get() = launcherActivityInfo.user
val canUninstall: Boolean
get() = flags and ApplicationInfo.FLAG_SYSTEM == 0 && isMainProfile
private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
override val profile: AppProfile
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
override val isSystemApp: Boolean = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0
override val canUninstall: Boolean
get() = !isSystemApp && isMainProfile
override val domain: String = Domain
override val preferDetailsOverLaunch: Boolean = false
@@ -59,22 +73,10 @@ data class LauncherApp(
}
override val key: String
get() = if (isMainProfile) "${domain}://$`package`:$activity" else "${domain}://$`package`:$activity:${userSerialNumber}"
// For backwards compatibility, user serial number is not included in main profile
get() = if (isMainProfile) "${domain}://${componentName.packageName}:${componentName.packageName}"
else "${domain}://${componentName.packageName}:${componentName.className}:${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.65f,
color = 0xff3dda84.toInt(),
),
backgroundLayer = ColorLayer(0xff3dda84.toInt())
)
}
override suspend fun loadIcon(
context: Context,
@@ -132,7 +134,7 @@ data class LauncherApp(
}
try {
launcherApps.startMainActivity(
ComponentName(`package`, activity),
componentName,
launcherActivityInfo.user,
null,
options
@@ -145,11 +147,15 @@ data class LauncherApp(
return true
}
fun getStoreDetails(context: Context): StoreLink? {
override fun getStoreDetails(context: Context): StoreLink? {
val pm = context.packageManager
return try {
val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`)
getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`)
val installSourceInfo =
PackageManagerCompat.getInstallSource(pm, componentName.packageName)
getStoreLinkForInstaller(
installSourceInfo.initiatingPackageName,
componentName.packageName
)
} catch (e: PackageManager.NameNotFoundException) {
null
} catch (e: IllegalArgumentException) {
@@ -157,37 +163,33 @@ data class LauncherApp(
}
}
fun uninstall(context: Context) {
override fun uninstall(context: Context) {
val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:$`package`")
intent.data = Uri.parse("package:${componentName.packageName}")
context.startActivity(intent)
}
fun openAppInfo(context: Context) {
override fun openAppDetails(context: Context) {
val launcherApps = context.getSystemService<LauncherApps>()!!
launcherApps.startAppDetailsActivity(
ComponentName(`package`, activity),
getUser(),
componentName,
user,
null,
null
)
}
suspend fun shareApkFile(context: Context) {
override val canShareApk: Boolean = true
override suspend fun shareApkFile(context: Context) {
val launcherApps = context.getSystemService<LauncherApps>()!!
val fileCopy = java.io.File(
context.cacheDir,
"${`package`}-${version}.apk"
"${componentName.packageName}-${versionName}.apk"
)
withContext(Dispatchers.IO) {
try {
val user = getUser()
val info = if (user != null) {
launcherApps.getApplicationInfo(`package`, 0, user)
} else {
context.packageManager.getApplicationInfo(`package`, 0)
}
val info = launcherApps.getApplicationInfo(componentName.packageName, 0, user)
val file = java.io.File(info.publicSourceDir)
try {
@@ -212,6 +214,13 @@ data class LauncherApp(
}
}
override fun getActivityInfo(context: Context): ActivityInfo? {
if (isAtLeastApiLevel(31)) {
return launcherActivityInfo.activityInfo
}
return super.getActivityInfo(context)
}
companion object {
private fun getStoreLinkForInstaller(
installerPackage: String?,
@@ -225,18 +234,21 @@ data class LauncherApp(
"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
}
}
@@ -251,9 +263,8 @@ data class LauncherApp(
const val Domain = "app"
}
}
data class StoreLink(
val label: String,
val url: String
)
override fun getSerializer(): SearchableSerializer {
return LauncherAppSerializer()
}
}
@@ -1,8 +1,14 @@
package de.mm20.launcher2.applications
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.Application
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 applicationsModule = module {
single<AppRepository> { AppRepositoryImpl(androidContext()) }
factory<SearchableRepository<Application>>(named<Application>()) { AppRepositoryImpl(androidContext()) }
factory<AppRepository> { AppRepositoryImpl(androidContext()) }
factory<SearchableDeserializer>(named(LauncherApp.Domain)) { LauncherAppDeserializer(androidContext()) }
}