diff options
Diffstat (limited to 'core')
13 files changed, 853 insertions, 0 deletions
diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/.gitignore @@ -0,0 +1 @@ +/build
\ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000..0e53f22 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 E FOUNDATION + * + * 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/>. + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +group 'foundation.e' + +android { + compileSdkVersion buildConfig.compileSdk + + defaultConfig { + minSdkVersion buildConfig.minSdk + targetSdkVersion buildConfig.targetSdk + + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation( + libs.androidx.core.ktx, + libs.bundles.koin, + libs.kotlinx.coroutines + ) +} diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/core/consumer-rules.pro diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/core/proguard-rules.pro @@ -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
\ No newline at end of file diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a29e84c --- /dev/null +++ b/core/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2023 MURENA SAS + Copyright (C) 2022 E FOUNDATION + + 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/>. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="foundation.e.advancedprivacy.core" + > +</manifest>
\ No newline at end of file diff --git a/core/src/main/java/foundation/e/advancedprivacy/core/KoinModule.kt b/core/src/main/java/foundation/e/advancedprivacy/core/KoinModule.kt new file mode 100644 index 0000000..141da86 --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/core/KoinModule.kt @@ -0,0 +1,23 @@ +package foundation.e.advancedprivacy.core + +import foundation.e.advancedprivacy.data.repositories.AppListsRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named +import org.koin.dsl.module + +@OptIn(DelicateCoroutinesApi::class) +val coreModule = module { + single<CoroutineScope> { GlobalScope } + single { + AppListsRepository( + permissionsModule = get(), + dummySystemApp = get(named("DummySystemApp")), + dummyCompatibilityApp = get(named("DummyCompatibilityApp")), + context = androidContext(), + coroutineScope = get() + ) + } +} diff --git a/core/src/main/java/foundation/e/advancedprivacy/core/utils/CoroutinesUtils.kt b/core/src/main/java/foundation/e/advancedprivacy/core/utils/CoroutinesUtils.kt new file mode 100644 index 0000000..5344c6a --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/core/utils/CoroutinesUtils.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 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.core.utils + +import kotlinx.coroutines.CancellationException +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +inline fun <T> runSuspendCatching(block: () -> T): Result<T> { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + return runCatching(block).onFailure { + if (it is CancellationException) { + throw it + } + } +} + +inline fun <R, T : R> Result<T>.recoverSuspendCatching( + transform: (exception: Throwable) -> R +): Result<R> { + return when (val exception = exceptionOrNull()) { + null -> this + else -> runSuspendCatching { transform(exception) } + } +} 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 +} diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/AppOpModes.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/AppOpModes.kt new file mode 100644 index 0000000..3e0a261 --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/AppOpModes.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 E FOUNDATION + * + * 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.domain.entities + +import android.app.AppOpsManager.MODE_ALLOWED +import android.app.AppOpsManager.MODE_DEFAULT +import android.app.AppOpsManager.MODE_ERRORED +import android.app.AppOpsManager.MODE_FOREGROUND +import android.app.AppOpsManager.MODE_IGNORED +import android.os.Build + +enum class AppOpModes(val modeValue: Int) { + ALLOWED(MODE_ALLOWED), + IGNORED(MODE_IGNORED), + ERRORED(MODE_ERRORED), + DEFAULT(MODE_DEFAULT), + FOREGROUND(if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) MODE_ALLOWED else MODE_FOREGROUND); + + companion object { + private val byMode = mapOf( + FOREGROUND.modeValue to FOREGROUND, + ALLOWED.modeValue to ALLOWED, + IGNORED.modeValue to IGNORED, + ERRORED.modeValue to ERRORED, + DEFAULT.modeValue to DEFAULT, + ) + + fun getByModeValue(modeValue: Int): AppOpModes { + return byMode.get(modeValue) ?: DEFAULT + } + } +} diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/ApplicationDescription.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/ApplicationDescription.kt new file mode 100644 index 0000000..90b637f --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/ApplicationDescription.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2022 E FOUNDATION + * + * 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.domain.entities + +import android.graphics.drawable.Drawable + +/** + * Useful informations to identify and describe an application. + */ +data class ApplicationDescription( + val packageName: String, + val uid: Int, + val profileId: Int, + val profileType: ProfileType, + var label: CharSequence?, + var icon: Drawable? +) { + val profileFlag = when (profileType) { + ProfileType.MAIN -> PROFILE_FLAG_MAIN + ProfileType.WORK -> PROFILE_FLAG_WORK + else -> profileId + } + + val apId: String get() = "${profileFlag}_$packageName" + + companion object { + const val PROFILE_FLAG_MAIN = -1 + const val PROFILE_FLAG_WORK = -2 + } +} + +enum class ProfileType { + MAIN, WORK, OTHER +} diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/PermissionDescription.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/PermissionDescription.kt new file mode 100644 index 0000000..c3899a9 --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/PermissionDescription.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 E FOUNDATION + * + * 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.domain.entities + +data class PermissionDescription( + val name: String, + var isDangerous: Boolean, + val group: String?, + var label: CharSequence?, + var description: CharSequence? +) diff --git a/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt b/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt new file mode 100644 index 0000000..78f424b --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/APermissionsPrivacyModule.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2022 - 2023 MURENA SAS + * Copyright (C) 2021 E FOUNDATION + * + * 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.externalinterfaces.permissions + +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PermissionInfo +import android.content.pm.PermissionInfo.PROTECTION_DANGEROUS +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.Log +import foundation.e.advancedprivacy.domain.entities.AppOpModes +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.domain.entities.PermissionDescription +import foundation.e.advancedprivacy.domain.entities.ProfileType + +/** + * Implementation of the commons functionality between privileged and standard + * versions of the module. + * @param context an Android context, to retrieve packageManager for example. + */ +abstract class APermissionsPrivacyModule(protected val context: Context) : IPermissionsPrivacyModule { + + companion object { + private const val TAG = "PermissionsModule" + } + + /** + * @see IPermissionsPrivacyModule.getInstalledApplications + */ + override fun getApplicationDescription(packageName: String, withIcon: Boolean): ApplicationDescription { + val appDesc = buildApplicationDescription(context.packageManager.getApplicationInfo(packageName, 0)) + if (withIcon) { + appDesc.icon = getApplicationIcon(appDesc.packageName) + } + return appDesc + } + + /** + * * @see IPermissionsPrivacyModule.getPermissions + */ + override fun getPermissions(packageName: String): List<String> { + val packageInfo = context.packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS) + return packageInfo.requestedPermissions?.asList() ?: emptyList() + } + + override fun getPermissionDescription(permissionName: String): PermissionDescription { + val info = context.packageManager.getPermissionInfo(permissionName, 0) + return PermissionDescription( + name = permissionName, + isDangerous = isPermissionsDangerous(info), + group = null, + label = info.loadLabel(context.packageManager), + description = info.loadDescription(context.packageManager) + ) + } + + /** + * @see IPermissionsPrivacyModule.isDangerousPermissionGranted + */ + override fun isDangerousPermissionGranted(packageName: String, permissionName: String): Boolean { + return context.packageManager + .checkPermission(permissionName, packageName) == PackageManager.PERMISSION_GRANTED + } + + // on google version, work only for the current package. + @Suppress("DEPRECATION") + override fun getAppOpMode( + appDesc: ApplicationDescription, + appOpPermissionName: String + ): AppOpModes { + + val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + + val mode = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + appOps.checkOpNoThrow( + appOpPermissionName, + + appDesc.uid, appDesc.packageName + ) + } else { + appOps.unsafeCheckOpNoThrow( + appOpPermissionName, + appDesc.uid, appDesc.packageName + ) + } + + return AppOpModes.getByModeValue(mode) + } + + override fun isPermissionsDangerous(permissionName: String): Boolean { + try { + val permissionInfo = context.packageManager.getPermissionInfo(permissionName, 0) + return isPermissionsDangerous(permissionInfo) + } catch (e: Exception) { + Log.w(TAG, "exception in isPermissionsDangerous(String)", e) + return false + } + } + + @Suppress("DEPRECATION") + private fun isPermissionsDangerous(permissionInfo: PermissionInfo): Boolean { + try { + return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + permissionInfo.protectionLevel and PROTECTION_DANGEROUS == 1 + } else { + permissionInfo.protection == PROTECTION_DANGEROUS + } + } catch (e: Exception) { + Log.w(TAG, "exception in isPermissionsDangerous(PermissionInfo)", e) + return false + } + } + + override fun buildApplicationDescription( + appInfo: ApplicationInfo, + profileId: Int, + profileType: ProfileType + ): + ApplicationDescription { + return ApplicationDescription( + packageName = appInfo.packageName, + uid = appInfo.uid, + label = getAppLabel(appInfo), + icon = null, + profileId = profileId, + profileType = profileType, + ) + } + + private fun getAppLabel(appInfo: ApplicationInfo): CharSequence { + return context.packageManager.getApplicationLabel(appInfo) + } + + fun getApplicationIcon(appInfo: ApplicationInfo): Drawable? { + return context.packageManager.getApplicationIcon(appInfo) + } + + override fun getApplicationIcon(packageName: String): Drawable? { + return context.packageManager.getApplicationIcon(packageName) + } +} diff --git a/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/IPermissionsPrivacyModule.kt b/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/IPermissionsPrivacyModule.kt new file mode 100644 index 0000000..da11769 --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/externalinterfaces/permissions/IPermissionsPrivacyModule.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2022 - 2023 MURENA SAS + * Copyright (C) 2021 E FOUNDATION + * + * 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.externalinterfaces.permissions + +import android.app.NotificationChannel +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.graphics.drawable.Drawable +import foundation.e.advancedprivacy.domain.entities.AppOpModes +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.domain.entities.PermissionDescription +import foundation.e.advancedprivacy.domain.entities.ProfileType + +/** + * List applications and manage theirs permissions. + */ +interface IPermissionsPrivacyModule { + + fun buildApplicationDescription( + appInfo: ApplicationInfo, + profileId: Int = -1, + profileType: ProfileType = ProfileType.MAIN + ): ApplicationDescription + + fun getApplications( + filter: ((PackageInfo) -> Boolean)?, + ): List<ApplicationDescription> + + /** + * List of permissions names used by an app, specified by its [packageName]. + * @param packageName the appId of the app + * @return the list off permission, in the "android.permission.PERMISSION" format. + */ + fun getPermissions(packageName: String): List<String> + + fun getPermissionDescription(permissionName: String): PermissionDescription + + /** + * Get the filled up [ApplicationDescription] for the app specified by its [packageName] + * @param packageName the appId of the app + * @return the informations about the app. + */ + fun getApplicationDescription(packageName: String, withIcon: Boolean = true): ApplicationDescription + + /** + * Check if the current runtime permission is granted for the specified app. + * + * @param packageName the packageName of the app + * @param permissionName the name of the permission in "android.permission.PERMISSION" format. + * @return the current status for this permission. + */ + fun isDangerousPermissionGranted(packageName: String, permissionName: String): Boolean + + /** + * Get the appOps mode for the specified [appOpPermissionName] of the specified application. + * + * @param appDesc the application + * @param appOpPermissionName the AppOps permission name. + * @return mode, as a [AppOpModes] + */ + fun getAppOpMode(appDesc: ApplicationDescription, appOpPermissionName: String): AppOpModes + + /** + * Grant or revoke the specified permission for the specigfied app. + * If their is not enough privileges to get the permission, return the false + * + * @param appDesc the application + * @param permissionName the name of the permission in "android.permission.PERMISSION" format. + * @param grant true grant the permission, false revoke it. + * @return true if the permission is or has just been granted, false if + * user has to do it himself. + */ + fun toggleDangerousPermission( + appDesc: ApplicationDescription, + permissionName: String, + grant: Boolean + ): Boolean + + /** + * Change the appOp Mode for the specified appOpPermission and application. + * @param appDesc the application + * @param appOpPermissionName the AppOps permission name. + * @return true if the mode has been changed, false if + * user has to do it himself. + */ + fun setAppOpMode( + appDesc: ApplicationDescription, + appOpPermissionName: String, + status: AppOpModes + ): Boolean + + /** + * Return true if the application is flagged Dangerous. + */ + fun isPermissionsDangerous(permissionName: String): Boolean + + /** + * Get the application icon. + */ + fun getApplicationIcon(packageName: String): Drawable? + + /** + * Get the application icon. + */ + fun getApplicationIcon(app: ApplicationDescription): Drawable? + + /** + * Authorize the specified package to be used as Vpn. + * @return true if authorization has been set, false if an error has occurred. + */ + fun setVpnPackageAuthorization(packageName: String): Boolean + + /** + * Returns the package name of the currently set always-on VPN application, or null. + */ + fun getAlwaysOnVpnPackage(): String? + + /** + * Allows users to block notifications sent through this channel, if this channel belongs to + * a package that is signed with the system signature. + * + * If the channel does not belong to a package that is signed with the system signature, this + * method does nothing, since such channels are blockable by default and cannot be set to be + * unblockable. + */ + fun setBlockable(notificationChannel: NotificationChannel) +} |