diff options
Diffstat (limited to 'core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt')
-rw-r--r-- | core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt b/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt new file mode 100644 index 0000000..f29bb8a --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2022 E FOUNDATION, 2022 - 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package foundation.e.advancedprivacy.data.repositories + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.domain.entities.ProfileType +import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule +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.runBlocking + +class AppListsRepository( + private val permissionsModule: IPermissionsPrivacyModule, + val dummySystemApp: ApplicationDescription, + val dummyCompatibilityApp: ApplicationDescription, + private val context: Context, + private val coroutineScope: CoroutineScope +) { + companion object { + private const val PNAME_SETTINGS = "com.android.settings" + private const val PNAME_PWAPLAYER = "foundation.e.pwaplayer" + private const val PNAME_INTENT_VERIFICATION = "com.android.statementservice" + private const val PNAME_MICROG_SERVICES_CORE = "com.google.android.gms" + + val compatibiltyPNames = setOf( + PNAME_PWAPLAYER, PNAME_INTENT_VERIFICATION, PNAME_MICROG_SERVICES_CORE + ) + } + + private suspend fun fetchAppDescriptions(fetchMissingIcons: Boolean = false) { + val launcherPackageNames = context.packageManager.queryIntentActivities( + Intent(Intent.ACTION_MAIN, null).apply { addCategory(Intent.CATEGORY_LAUNCHER) }, + 0 + ).mapNotNull { it.activityInfo?.packageName } + + val visibleAppsFilter = { packageInfo: PackageInfo -> + hasInternetPermission(packageInfo) && + isStandardApp(packageInfo.applicationInfo, launcherPackageNames) + } + + val hiddenAppsFilter = { packageInfo: PackageInfo -> + hasInternetPermission(packageInfo) && + isHiddenSystemApp(packageInfo.applicationInfo, launcherPackageNames) + } + + val compatibilityAppsFilter = { packageInfo: PackageInfo -> + packageInfo.packageName in compatibiltyPNames + } + + val visibleApps = recycleIcons( + newApps = permissionsModule.getApplications(visibleAppsFilter), + fetchMissingIcons = fetchMissingIcons + ) + val hiddenApps = permissionsModule.getApplications(hiddenAppsFilter) + val compatibilityApps = permissionsModule.getApplications(compatibilityAppsFilter) + + updateMaps(visibleApps + hiddenApps + compatibilityApps) + + allProfilesAppDescriptions.emit( + Triple( + visibleApps + dummySystemApp + dummyCompatibilityApp, + hiddenApps, + compatibilityApps + ) + ) + } + + private fun recycleIcons( + newApps: List<ApplicationDescription>, + fetchMissingIcons: Boolean + ): List<ApplicationDescription> { + val oldVisibleApps = allProfilesAppDescriptions.value.first + return newApps.map { app -> + app.copy( + icon = oldVisibleApps.find { app.apId == it.apId }?.icon + ?: if (fetchMissingIcons) permissionsModule.getApplicationIcon(app) else null + ) + } + } + + private fun updateMaps(apps: List<ApplicationDescription>) { + val byUid = mutableMapOf<Int, ApplicationDescription>() + val byApId = mutableMapOf<String, ApplicationDescription>() + apps.forEach { app -> + byUid[app.uid]?.run { packageName > app.packageName } == true + if (byUid[app.uid].let { it == null || it.packageName > app.packageName }) { + byUid[app.uid] = app + } + + byApId[app.apId] = app + } + appsByUid = byUid + appsByAPId = byApId + } + + private var lastFetchApps = 0 + private var refreshAppJob: Job? = null + private fun refreshAppDescriptions(fetchMissingIcons: Boolean = true, force: Boolean = false): Job? { + if (refreshAppJob == null || refreshAppJob?.isCompleted == true) { + refreshAppJob = coroutineScope.launch(Dispatchers.IO) { + if (appsByUid.isEmpty() || appsByAPId.isEmpty() || + force || context.packageManager.getChangedPackages(lastFetchApps) != null + ) { + fetchAppDescriptions(fetchMissingIcons = fetchMissingIcons) + if (fetchMissingIcons) { + lastFetchApps = context.packageManager.getChangedPackages(lastFetchApps) + ?.sequenceNumber ?: lastFetchApps + } + } + } + } + + return refreshAppJob + } + + fun mainProfileApps(): Flow<List<ApplicationDescription>> { + refreshAppDescriptions() + return allProfilesAppDescriptions.map { + it.first.filter { app -> app.profileType == ProfileType.MAIN } + .sortedBy { app -> app.label.toString().lowercase() } + } + } + + fun getMainProfileHiddenSystemApps(): List<ApplicationDescription> { + return allProfilesAppDescriptions.value.second.filter { it.profileType == ProfileType.MAIN } + } + + fun apps(): Flow<List<ApplicationDescription>> { + refreshAppDescriptions() + return allProfilesAppDescriptions.map { + it.first.sortedBy { app -> app.label.toString().lowercase() } + } + } + + fun allApps(): Flow<List<ApplicationDescription>> { + return allProfilesAppDescriptions.map { + it.first + it.second + it.third + } + } + + private fun getHiddenSystemApps(): List<ApplicationDescription> { + return allProfilesAppDescriptions.value.second + } + + private fun getCompatibilityApps(): List<ApplicationDescription> { + return allProfilesAppDescriptions.value.third + } + + fun anyForHiddenApps(app: ApplicationDescription, test: (ApplicationDescription) -> Boolean): Boolean { + return if (app == dummySystemApp) { + getHiddenSystemApps().any { test(it) } + } else if (app == dummyCompatibilityApp) { + getCompatibilityApps().any { test(it) } + } else test(app) + } + + fun applyForHiddenApps(app: ApplicationDescription, action: (ApplicationDescription) -> Unit) { + mapReduceForHiddenApps(app = app, map = action, reduce = {}) + } + + fun <T, R> mapReduceForHiddenApps( + app: ApplicationDescription, + map: (ApplicationDescription) -> T, + reduce: (List<T>) -> R + ): R { + return if (app == dummySystemApp) { + reduce(getHiddenSystemApps().map(map)) + } else if (app == dummyCompatibilityApp) { + reduce(getCompatibilityApps().map(map)) + } else reduce(listOf(map(app))) + } + + private var appsByUid = mapOf<Int, ApplicationDescription>() + private var appsByAPId = mapOf<String, ApplicationDescription>() + + fun getApp(appUid: Int): ApplicationDescription? { + return appsByUid[appUid] ?: run { + runBlocking { refreshAppDescriptions(fetchMissingIcons = false, force = true)?.join() } + appsByUid[appUid] + } + } + + fun getApp(apId: String): ApplicationDescription? { + if (apId.isBlank()) return null + + return appsByAPId[apId] ?: run { + runBlocking { refreshAppDescriptions(fetchMissingIcons = false, force = true)?.join() } + appsByAPId[apId] + } + } + + private val allProfilesAppDescriptions = MutableStateFlow( + Triple( + emptyList<ApplicationDescription>(), + emptyList<ApplicationDescription>(), + emptyList<ApplicationDescription>() + ) + ) + + private fun hasInternetPermission(packageInfo: PackageInfo): Boolean { + return packageInfo.requestedPermissions?.contains(Manifest.permission.INTERNET) == true + } + + @Suppress("ReturnCount") + private fun isNotHiddenSystemApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { + if (app.packageName == PNAME_SETTINGS) { + return false + } else if (app.packageName == PNAME_PWAPLAYER) { + return true + } else if (app.hasFlag(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) { + return true + } else if (!app.hasFlag(ApplicationInfo.FLAG_SYSTEM)) { + return true + } else if (launcherApps.contains(app.packageName)) { + return true + } + return false + } + + private fun isStandardApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { + return when { + app.packageName == PNAME_SETTINGS -> false + app.packageName in compatibiltyPNames -> false + app.hasFlag(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) -> true + !app.hasFlag(ApplicationInfo.FLAG_SYSTEM) -> true + launcherApps.contains(app.packageName) -> true + else -> false + } + } + + private fun isHiddenSystemApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { + return when { + app.packageName in compatibiltyPNames -> false + else -> !isNotHiddenSystemApp(app, launcherApps) + } + } + + private fun ApplicationInfo.hasFlag(flag: Int) = (flags and flag) == 1 +} |