diff options
Diffstat (limited to 'app/src/main/java/foundation/e/advancedprivacy/data')
4 files changed, 572 insertions, 0 deletions
diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt new file mode 100644 index 0000000..0b951a8 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt @@ -0,0 +1,281 @@ +/* + * 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.R +import foundation.e.privacymodules.permissions.PermissionsPrivacyModule +import foundation.e.privacymodules.permissions.data.ApplicationDescription +import foundation.e.privacymodules.permissions.data.ProfileType +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: PermissionsPrivacyModule, + 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 + ) + } + + val dummySystemApp = ApplicationDescription( + packageName = "foundation.e.dummysystemapp", + uid = -1, + label = context.getString(R.string.dummy_system_app_label), + icon = context.getDrawable(R.drawable.ic_e_app_logo), + profileId = -1, + profileType = ProfileType.MAIN + ) + + val dummyCompatibilityApp = ApplicationDescription( + packageName = "foundation.e.dummyappscompatibilityapp", + uid = -2, + label = context.getString(R.string.dummy_apps_compatibility_app_label), + icon = context.getDrawable(R.drawable.ic_apps_compatibility_components), + profileId = -1, + profileType = ProfileType.MAIN + ) + + 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 = coroutineScope.launch(Dispatchers.IO) { + if (force || context.packageManager.getChangedPackages(lastFetchApps) != null) { + fetchAppDescriptions(fetchMissingIcons = fetchMissingIcons) + if (fetchMissingIcons) { + lastFetchApps = context.packageManager.getChangedPackages(lastFetchApps) + ?.sequenceNumber ?: lastFetchApps + } + + refreshAppJob = null + } + } + } + + 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/app/src/main/java/foundation/e/advancedprivacy/data/repositories/CityDataSource.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/CityDataSource.kt new file mode 100644 index 0000000..06fb9ac --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/CityDataSource.kt @@ -0,0 +1,46 @@ +/* + * 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.dummy + +object CityDataSource { + private val BARCELONA = Pair(41.3851f, 2.1734f) + private val BUDAPEST = Pair(47.4979f, 19.0402f) + private val ABU_DHABI = Pair(24.4539f, 54.3773f) + private val HYDERABAD = Pair(17.3850f, 78.4867f) + private val QUEZON_CITY = Pair(14.6760f, 121.0437f) + private val PARIS = Pair(48.8566f, 2.3522f) + private val LONDON = Pair(51.5074f, 0.1278f) + private val SHANGHAI = Pair(31.2304f, 121.4737f) + private val MADRID = Pair(40.4168f, -3.7038f) + private val LAHORE = Pair(31.5204f, 74.3587f) + private val CHICAGO = Pair(41.8781f, -87.6298f) + + val citiesLocationsList = listOf( + BARCELONA, + BUDAPEST, + ABU_DHABI, + HYDERABAD, + QUEZON_CITY, + PARIS, + LONDON, + SHANGHAI, + MADRID, + LAHORE, + CHICAGO + ) +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt new file mode 100644 index 0000000..3f73c78 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt @@ -0,0 +1,116 @@ +/* + * 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.data.repositories + +import android.content.Context +import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode +import foundation.e.advancedprivacy.domain.entities.LocationMode +import foundation.e.privacymodules.permissions.data.ApplicationDescription +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class LocalStateRepository(context: Context) { + companion object { + private const val SHARED_PREFS_FILE = "localState" + private const val KEY_BLOCK_TRACKERS = "blockTrackers" + private const val KEY_IP_SCRAMBLING = "ipScrambling" + private const val KEY_FAKE_LOCATION = "fakeLocation" + private const val KEY_FAKE_LATITUDE = "fakeLatitude" + private const val KEY_FAKE_LONGITUDE = "fakeLongitude" + private const val KEY_FIRST_BOOT = "firstBoot" + private const val KEY_HIDE_WARNING_TRACKERS = "hide_warning_trackers" + private const val KEY_HIDE_WARNING_LOCATION = "hide_warning_location" + private const val KEY_HIDE_WARNING_IPSCRAMBLING = "hide_warning_ipscrambling" + } + + private val sharedPref = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE) + + private val _blockTrackers = MutableStateFlow(sharedPref.getBoolean(KEY_BLOCK_TRACKERS, true)) + val blockTrackers = _blockTrackers.asStateFlow() + + fun setBlockTrackers(enabled: Boolean) { + set(KEY_BLOCK_TRACKERS, enabled) + _blockTrackers.update { enabled } + } + + val areAllTrackersBlocked: MutableStateFlow<Boolean> = MutableStateFlow(false) + + private val _fakeLocationEnabled = MutableStateFlow(sharedPref.getBoolean(KEY_FAKE_LOCATION, false)) + + val fakeLocationEnabled = _fakeLocationEnabled.asStateFlow() + + fun setFakeLocationEnabled(enabled: Boolean) { + set(KEY_FAKE_LOCATION, enabled) + _fakeLocationEnabled.update { enabled } + } + + var fakeLocation: Pair<Float, Float> + get() = Pair( + // Initial default value is Quezon City + sharedPref.getFloat(KEY_FAKE_LATITUDE, 14.6760f), + sharedPref.getFloat(KEY_FAKE_LONGITUDE, 121.0437f) + ) + + set(value) { + sharedPref.edit() + .putFloat(KEY_FAKE_LATITUDE, value.first) + .putFloat(KEY_FAKE_LONGITUDE, value.second) + .apply() + } + + val locationMode: MutableStateFlow<LocationMode> = MutableStateFlow(LocationMode.REAL_LOCATION) + + private val _ipScramblingSetting = MutableStateFlow(sharedPref.getBoolean(KEY_IP_SCRAMBLING, false)) + val ipScramblingSetting = _ipScramblingSetting.asStateFlow() + + fun setIpScramblingSetting(enabled: Boolean) { + set(KEY_IP_SCRAMBLING, enabled) + _ipScramblingSetting.update { enabled } + } + + val internetPrivacyMode: MutableStateFlow<InternetPrivacyMode> = MutableStateFlow(InternetPrivacyMode.REAL_IP) + + private val _otherVpnRunning = MutableSharedFlow<ApplicationDescription>() + suspend fun emitOtherVpnRunning(appDesc: ApplicationDescription) { + _otherVpnRunning.emit(appDesc) + } + val otherVpnRunning: SharedFlow<ApplicationDescription> = _otherVpnRunning + + var firstBoot: Boolean + get() = sharedPref.getBoolean(KEY_FIRST_BOOT, true) + set(value) = set(KEY_FIRST_BOOT, value) + + var hideWarningTrackers: Boolean + get() = sharedPref.getBoolean(KEY_HIDE_WARNING_TRACKERS, false) + set(value) = set(KEY_HIDE_WARNING_TRACKERS, value) + + var hideWarningLocation: Boolean + get() = sharedPref.getBoolean(KEY_HIDE_WARNING_LOCATION, false) + set(value) = set(KEY_HIDE_WARNING_LOCATION, value) + + var hideWarningIpScrambling: Boolean + get() = sharedPref.getBoolean(KEY_HIDE_WARNING_IPSCRAMBLING, false) + set(value) = set(KEY_HIDE_WARNING_IPSCRAMBLING, value) + + private fun set(key: String, value: Boolean) { + sharedPref.edit().putBoolean(key, value).apply() + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt new file mode 100644 index 0000000..82915df --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt @@ -0,0 +1,129 @@ +/* + * 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.data.repositories + +import android.content.Context +import android.util.Log +import com.google.gson.Gson +import foundation.e.privacymodules.trackers.api.Tracker +import retrofit2.Retrofit +import retrofit2.converter.scalars.ScalarsConverterFactory +import retrofit2.http.GET +import java.io.File +import java.io.FileInputStream +import java.io.FileWriter +import java.io.IOException +import java.io.InputStreamReader +import java.io.PrintWriter + +class TrackersRepository(private val context: Context) { + + private val eTrackerFileName = "e_trackers.json" + private val eTrackerFile = File(context.filesDir.absolutePath, eTrackerFileName) + + var trackers: List<Tracker> = emptyList() + private set + + init { + initTrackersFile() + } + + suspend fun update() { + val api = ETrackersApi.build() + saveData(eTrackerFile, api.trackers()) + initTrackersFile() + } + + private fun initTrackersFile() { + try { + var inputStream = context.assets.open(eTrackerFileName) + if (eTrackerFile.exists()) { + inputStream = FileInputStream(eTrackerFile) + } + val reader = InputStreamReader(inputStream, "UTF-8") + val trackerResponse = + Gson().fromJson(reader, ETrackersApi.ETrackersResponse::class.java) + + trackers = mapper(trackerResponse) + + reader.close() + inputStream.close() + } catch (e: Exception) { + Log.e("TrackersRepository", "While parsing trackers in assets", e) + } + } + + private fun mapper(response: ETrackersApi.ETrackersResponse): List<Tracker> { + return response.trackers.mapNotNull { + try { + it.toTracker() + } catch (e: Exception) { + null + } + } + } + + private fun ETrackersApi.ETrackersResponse.ETracker.toTracker(): Tracker { + return Tracker( + id = id!!, + hostnames = hostnames!!.toSet(), + label = name!!, + exodusId = exodusId + ) + } + + private fun saveData(file: File, data: String): Boolean { + try { + val fos = FileWriter(file, false) + val ps = PrintWriter(fos) + ps.apply { + print(data) + flush() + close() + } + return true + } catch (e: IOException) { + e.printStackTrace() + } + return false + } +} + +interface ETrackersApi { + companion object { + fun build(): ETrackersApi { + val retrofit = Retrofit.Builder() + .baseUrl("https://gitlab.e.foundation/e/os/tracker-list/-/raw/main/") + .addConverterFactory(ScalarsConverterFactory.create()) + .build() + return retrofit.create(ETrackersApi::class.java) + } + } + + @GET("list/e_trackers.json") + suspend fun trackers(): String + + data class ETrackersResponse(val trackers: List<ETracker>) { + data class ETracker( + val id: String?, + val hostnames: List<String>?, + val name: String?, + val exodusId: String? + ) + } +} |