summaryrefslogtreecommitdiff
path: root/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt
diff options
context:
space:
mode:
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.kt264
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
+}