summaryrefslogtreecommitdiff
path: root/app/src/main/java/foundation/e/advancedprivacy
diff options
context:
space:
mode:
authorGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-05-09 06:00:43 +0000
committerGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-05-09 06:00:43 +0000
commit5a432ecde520ee039786848296e5227571404158 (patch)
tree077eafb42d5d2d18b2ffc03bc93d9a8654377774 /app/src/main/java/foundation/e/advancedprivacy
parenta348c8196a643e4f5853587133b05e3ec2cd0faa (diff)
parenta8874167f663885f2d3371801cf03681576ac817 (diff)
Merge branch '1200-move_package_to_advanced_privacy' into 'main'
1200: rename everything to AdvancedPrivacy See merge request e/os/advanced-privacy!128
Diffstat (limited to 'app/src/main/java/foundation/e/advancedprivacy')
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt34
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt211
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/Notifications.kt210
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/UpdateTrackersWorker.kt59
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt71
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt36
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt23
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt333
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt33
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt43
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt63
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt36
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt76
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt45
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt130
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt22
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt281
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/data/repositories/CityDataSource.kt46
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt116
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt133
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/entities/AppWithCounts.kt59
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt29
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt22
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/entities/MainFeatures.kt22
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/entities/QuickPrivacyState.kt24
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackerMode.kt22
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt25
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppListUseCase.kt39
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt209
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt89
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt170
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt54
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt105
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt278
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/UpdateWidgetUseCase.kt33
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt307
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt37
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt158
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt201
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt36
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt157
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt376
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt53
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt29
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt126
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt218
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt28
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt95
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt189
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt42
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt172
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt92
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/main/MainActivity.kt106
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt156
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt42
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt381
56 files changed, 6182 insertions, 0 deletions
diff --git a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt
new file mode 100644
index 0000000..9ce0c2b
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.app.Application
+import foundation.e.lib.telemetry.Telemetry
+
+class AdvancedPrivacyApplication : Application() {
+
+ // Initialize the dependency container.
+ val dependencyContainer: DependencyContainer by lazy { DependencyContainer(this) }
+
+ override fun onCreate() {
+ super.onCreate()
+ Telemetry.init(BuildConfig.SENTRY_DSN, this, true)
+
+ dependencyContainer.initBackgroundSingletons()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt b/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt
new file mode 100644
index 0000000..91e2f44
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 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
+
+import android.app.Application
+import android.content.Context
+import android.os.Process
+import androidx.lifecycle.DEFAULT_ARGS_KEY
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.CreationExtras
+import foundation.e.advancedprivacy.common.WarningDialog
+import foundation.e.advancedprivacy.data.repositories.AppListsRepository
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+import foundation.e.advancedprivacy.data.repositories.TrackersRepository
+import foundation.e.advancedprivacy.domain.usecases.AppListUseCase
+import foundation.e.advancedprivacy.domain.usecases.FakeLocationStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.IpScramblingStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.advancedprivacy.dummy.CityDataSource
+import foundation.e.advancedprivacy.features.dashboard.DashboardViewModel
+import foundation.e.advancedprivacy.features.internetprivacy.InternetPrivacyViewModel
+import foundation.e.advancedprivacy.features.location.FakeLocationViewModel
+import foundation.e.advancedprivacy.features.trackers.TrackersViewModel
+import foundation.e.advancedprivacy.features.trackers.apptrackers.AppTrackersFragment
+import foundation.e.advancedprivacy.features.trackers.apptrackers.AppTrackersViewModel
+import foundation.e.privacymodules.fakelocation.FakeLocationModule
+import foundation.e.privacymodules.ipscrambler.IpScramblerModule
+import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
+import foundation.e.privacymodules.permissions.PermissionsPrivacyModule
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import foundation.e.privacymodules.permissions.data.ProfileType
+import foundation.e.privacymodules.trackers.api.BlockTrackersPrivacyModule
+import foundation.e.privacymodules.trackers.api.TrackTrackersPrivacyModule
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+
+/**
+ * Simple container to hold application wide dependencies.
+ *
+ */
+@OptIn(DelicateCoroutinesApi::class)
+class DependencyContainer(val app: Application) {
+ val context: Context by lazy { app.applicationContext }
+
+ // Drivers
+ private val fakeLocationModule: FakeLocationModule by lazy { FakeLocationModule(app.applicationContext) }
+ private val permissionsModule by lazy { PermissionsPrivacyModule(app.applicationContext) }
+ private val ipScramblerModule: IIpScramblerModule by lazy { IpScramblerModule(app.applicationContext) }
+
+ private val appDesc by lazy {
+ ApplicationDescription(
+ packageName = context.packageName,
+ uid = Process.myUid(),
+ label = context.resources.getString(R.string.app_name),
+ icon = null,
+ profileId = -1,
+ profileType = ProfileType.MAIN
+ )
+ }
+
+ private val blockTrackersPrivacyModule by lazy { BlockTrackersPrivacyModule.getInstance(context) }
+ private val trackTrackersPrivacyModule by lazy { TrackTrackersPrivacyModule.getInstance(context) }
+
+ // Repositories
+ private val localStateRepository by lazy { LocalStateRepository(context) }
+ private val trackersRepository by lazy { TrackersRepository(context) }
+ private val appListsRepository by lazy { AppListsRepository(permissionsModule, context, GlobalScope) }
+
+ // Usecases
+ val getQuickPrivacyStateUseCase by lazy {
+ GetQuickPrivacyStateUseCase(localStateRepository)
+ }
+ private val ipScramblingStateUseCase by lazy {
+ IpScramblingStateUseCase(
+ ipScramblerModule, permissionsModule, appDesc, localStateRepository,
+ appListsRepository, GlobalScope
+ )
+ }
+ private val appListUseCase = AppListUseCase(appListsRepository)
+
+ val trackersStatisticsUseCase by lazy {
+ TrackersStatisticsUseCase(trackTrackersPrivacyModule, blockTrackersPrivacyModule, appListsRepository, context.resources)
+ }
+
+ val trackersStateUseCase by lazy {
+ TrackersStateUseCase(blockTrackersPrivacyModule, trackTrackersPrivacyModule, localStateRepository, trackersRepository, appListsRepository, GlobalScope)
+ }
+
+ private val fakeLocationStateUseCase by lazy {
+ FakeLocationStateUseCase(
+ fakeLocationModule, permissionsModule, localStateRepository, CityDataSource, appDesc, context, GlobalScope
+ )
+ }
+
+ val showFeaturesWarningUseCase by lazy {
+ ShowFeaturesWarningUseCase(localStateRepository = localStateRepository)
+ }
+
+ val viewModelsFactory by lazy {
+ ViewModelsFactory(
+ getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
+ trackersStatisticsUseCase = trackersStatisticsUseCase,
+ trackersStateUseCase = trackersStateUseCase,
+ fakeLocationStateUseCase = fakeLocationStateUseCase,
+ ipScramblerModule = ipScramblerModule,
+ ipScramblingStateUseCase = ipScramblingStateUseCase,
+ appListUseCase = appListUseCase
+ )
+ }
+
+ // Background
+ fun initBackgroundSingletons() {
+ trackersStateUseCase
+ ipScramblingStateUseCase
+ fakeLocationStateUseCase
+
+ UpdateTrackersWorker.periodicUpdate(context)
+
+ WarningDialog.startListening(
+ showFeaturesWarningUseCase,
+ GlobalScope,
+ context
+ )
+
+ Widget.startListening(
+ context,
+ getQuickPrivacyStateUseCase,
+ trackersStatisticsUseCase,
+ )
+
+ Notifications.startListening(
+ context,
+ getQuickPrivacyStateUseCase,
+ permissionsModule,
+ GlobalScope
+ )
+ }
+}
+
+@Suppress("LongParameterList")
+class ViewModelsFactory(
+ private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
+ private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
+ private val trackersStateUseCase: TrackersStateUseCase,
+ private val fakeLocationStateUseCase: FakeLocationStateUseCase,
+ private val ipScramblerModule: IIpScramblerModule,
+ private val ipScramblingStateUseCase: IpScramblingStateUseCase,
+ private val appListUseCase: AppListUseCase
+) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
+ return when (modelClass) {
+ AppTrackersViewModel::class.java -> {
+ val app = extras[DEFAULT_ARGS_KEY]?.getInt(AppTrackersFragment.PARAM_APP_UID)?.let {
+ appListUseCase.getApp(it)
+ } ?: appListUseCase.dummySystemApp
+
+ AppTrackersViewModel(
+ app = app,
+ trackersStateUseCase = trackersStateUseCase,
+ trackersStatisticsUseCase = trackersStatisticsUseCase,
+ getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase
+ )
+ }
+
+ TrackersViewModel::class.java ->
+ TrackersViewModel(
+ trackersStatisticsUseCase = trackersStatisticsUseCase
+ )
+ FakeLocationViewModel::class.java ->
+ FakeLocationViewModel(
+ getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
+ fakeLocationStateUseCase = fakeLocationStateUseCase
+ )
+ InternetPrivacyViewModel::class.java ->
+ InternetPrivacyViewModel(
+ ipScramblerModule = ipScramblerModule,
+ getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
+ ipScramblingStateUseCase = ipScramblingStateUseCase,
+ appListUseCase = appListUseCase
+ )
+ DashboardViewModel::class.java ->
+ DashboardViewModel(
+ getPrivacyStateUseCase = getQuickPrivacyStateUseCase,
+ trackersStatisticsUseCase = trackersStatisticsUseCase
+ )
+ else -> throw IllegalArgumentException("Unknown class $modelClass")
+ } as T
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt b/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt
new file mode 100644
index 0000000..68c4bd3
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import foundation.e.advancedprivacy.domain.entities.MainFeatures
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import foundation.e.advancedprivacy.main.MainActivity
+import foundation.e.privacymodules.permissions.PermissionsPrivacyModule
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+object Notifications {
+ const val CHANNEL_FIRST_BOOT = "first_boot_notification"
+ const val CHANNEL_FAKE_LOCATION_FLAG = "fake_location_flag"
+ const val CHANNEL_IPSCRAMBLING_FLAG = "ipscrambling_flag"
+
+ const val NOTIFICATION_FIRST_BOOT = 1000
+ const val NOTIFICATION_FAKE_LOCATION_FLAG = NOTIFICATION_FIRST_BOOT + 1
+ const val NOTIFICATION_IPSCRAMBLING_FLAG = NOTIFICATION_FAKE_LOCATION_FLAG + 1
+
+ fun showFirstBootNotification(context: Context) {
+ createNotificationFirstBootChannel(context)
+ val notificationBuilder: NotificationCompat.Builder = notificationBuilder(
+ context,
+ NotificationContent(
+ channelId = CHANNEL_FIRST_BOOT,
+ icon = R.drawable.ic_notification_logo,
+ title = R.string.first_notification_title,
+ description = R.string.first_notification_summary,
+ destinationIntent =
+ context.packageManager.getLaunchIntentForPackage(context.packageName)
+ )
+ )
+ .setAutoCancel(true)
+
+ NotificationManagerCompat.from(context).notify(
+ NOTIFICATION_FIRST_BOOT, notificationBuilder.build()
+ )
+ }
+
+ fun startListening(
+ appContext: Context,
+ getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
+ permissionsPrivacyModule: PermissionsPrivacyModule,
+ appScope: CoroutineScope
+ ) {
+ createNotificationFlagChannel(
+ context = appContext,
+ permissionsPrivacyModule = permissionsPrivacyModule,
+ channelId = CHANNEL_FAKE_LOCATION_FLAG,
+ channelName = R.string.notifications_fake_location_channel_name,
+ channelDescription = R.string.notifications_fake_location_channel_description
+ )
+
+ createNotificationFlagChannel(
+ context = appContext,
+ permissionsPrivacyModule = permissionsPrivacyModule,
+ channelId = CHANNEL_IPSCRAMBLING_FLAG,
+ channelName = R.string.notifications_ipscrambling_channel_name,
+ channelDescription = R.string.notifications_ipscrambling_channel_description
+ )
+
+ getQuickPrivacyStateUseCase.isLocationHidden.onEach {
+ if (it) {
+ showFlagNotification(appContext, MainFeatures.FAKE_LOCATION)
+ } else {
+ hideFlagNotification(appContext, MainFeatures.FAKE_LOCATION)
+ }
+ }.launchIn(appScope)
+
+ getQuickPrivacyStateUseCase.ipScramblingMode.map {
+ it != InternetPrivacyMode.REAL_IP
+ }.distinctUntilChanged().onEach {
+ if (it) {
+ showFlagNotification(appContext, MainFeatures.IP_SCRAMBLING)
+ } else {
+ hideFlagNotification(appContext, MainFeatures.IP_SCRAMBLING)
+ }
+ }.launchIn(appScope)
+ }
+
+ private fun createNotificationFirstBootChannel(context: Context) {
+ val channel = NotificationChannel(
+ CHANNEL_FIRST_BOOT,
+ context.getString(R.string.notifications_first_boot_channel_name),
+ NotificationManager.IMPORTANCE_HIGH
+ )
+ NotificationManagerCompat.from(context).createNotificationChannel(channel)
+ }
+
+ private fun createNotificationFlagChannel(
+ context: Context,
+ permissionsPrivacyModule: PermissionsPrivacyModule,
+ channelId: String,
+ @StringRes channelName: Int,
+ @StringRes channelDescription: Int,
+ ) {
+ val channel = NotificationChannel(
+ channelId, context.getString(channelName), NotificationManager.IMPORTANCE_LOW
+ )
+ channel.description = context.getString(channelDescription)
+ permissionsPrivacyModule.setBlockable(channel)
+ NotificationManagerCompat.from(context).createNotificationChannel(channel)
+ }
+
+ private fun showFlagNotification(context: Context, feature: MainFeatures) {
+ when (feature) {
+ MainFeatures.FAKE_LOCATION -> showFlagNotification(
+ context = context,
+ id = NOTIFICATION_FAKE_LOCATION_FLAG,
+ content = NotificationContent(
+ channelId = CHANNEL_FAKE_LOCATION_FLAG,
+ icon = R.drawable.ic_fmd_bad,
+ title = R.string.notifications_fake_location_title,
+ description = R.string.notifications_fake_location_content,
+ destinationIntent = MainActivity.createFakeLocationIntent(context),
+ )
+ )
+ MainFeatures.IP_SCRAMBLING -> showFlagNotification(
+ context = context,
+ id = NOTIFICATION_IPSCRAMBLING_FLAG,
+ content = NotificationContent(
+ channelId = CHANNEL_IPSCRAMBLING_FLAG,
+ icon = R.drawable.ic_language,
+ title = R.string.notifications_ipscrambling_title,
+ description = R.string.notifications_ipscrambling_content,
+ destinationIntent = MainActivity.createIpScramblingIntent(context),
+ )
+ )
+ else -> {}
+ }
+ }
+
+ private fun showFlagNotification(
+ context: Context,
+ id: Int,
+ content: NotificationContent,
+ ) {
+ val builder = notificationBuilder(context, content)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(true)
+
+ NotificationManagerCompat.from(context).notify(id, builder.build())
+ }
+
+ private fun hideFlagNotification(context: Context, feature: MainFeatures) {
+ val id = when (feature) {
+ MainFeatures.FAKE_LOCATION -> NOTIFICATION_FAKE_LOCATION_FLAG
+ MainFeatures.IP_SCRAMBLING -> NOTIFICATION_IPSCRAMBLING_FLAG
+ else -> return
+ }
+ NotificationManagerCompat.from(context).cancel(id)
+ }
+
+ private data class NotificationContent(
+ val channelId: String,
+ val icon: Int,
+ val title: Int,
+ val description: Int,
+ val destinationIntent: Intent?
+ )
+
+ private fun notificationBuilder(
+ context: Context,
+ content: NotificationContent
+ ): NotificationCompat.Builder {
+ val builder = NotificationCompat.Builder(context, content.channelId)
+ .setSmallIcon(content.icon)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setContentTitle(context.getString(content.title))
+ .setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(content.description)))
+
+ content.destinationIntent?.let {
+ it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context, 0, it, PendingIntent.FLAG_IMMUTABLE
+ )
+ builder.setContentIntent(pendingIntent)
+ }
+
+ return builder
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/UpdateTrackersWorker.kt b/app/src/main/java/foundation/e/advancedprivacy/UpdateTrackersWorker.kt
new file mode 100644
index 0000000..418f75b
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/UpdateTrackersWorker.kt
@@ -0,0 +1,59 @@
+/*
+ * 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
+
+import android.content.Context
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import java.util.concurrent.TimeUnit
+
+class UpdateTrackersWorker(appContext: Context, workerParams: WorkerParameters) :
+ CoroutineWorker(appContext, workerParams) {
+
+ override suspend fun doWork(): Result {
+ val trackersStateUseCase = (applicationContext as AdvancedPrivacyApplication)
+ .dependencyContainer.trackersStateUseCase
+
+ trackersStateUseCase.updateTrackers()
+ return Result.success()
+ }
+
+ companion object {
+ private val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ fun periodicUpdate(context: Context) {
+ val request = PeriodicWorkRequestBuilder<UpdateTrackersWorker>(
+ 7, TimeUnit.DAYS
+ )
+ .setConstraints(constraints).build()
+
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(
+ UpdateTrackersWorker::class.qualifiedName ?: "",
+ ExistingPeriodicWorkPolicy.KEEP,
+ request
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt
new file mode 100644
index 0000000..aee1890
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 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.common
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.domain.entities.AppWithCounts
+
+class AppsAdapter(
+ private val itemsLayout: Int,
+ private val listener: (Int) -> Unit
+) :
+ RecyclerView.Adapter<AppsAdapter.ViewHolder>() {
+
+ class ViewHolder(view: View, private val listener: (Int) -> Unit) : RecyclerView.ViewHolder(view) {
+ val appName: TextView = view.findViewById(R.id.title)
+ val counts: TextView = view.findViewById(R.id.counts)
+ val icon: ImageView = view.findViewById(R.id.icon)
+ fun bind(item: AppWithCounts) {
+ appName.text = item.label
+ counts.text = if (item.trackersCount > 0) itemView.context.getString(
+ R.string.trackers_app_trackers_counts,
+ item.blockedTrackersCount,
+ item.trackersCount,
+ item.leaks
+ ) else ""
+ icon.setImageDrawable(item.icon)
+
+ itemView.setOnClickListener { listener(item.uid) }
+ }
+ }
+
+ var dataSet: List<AppWithCounts> = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(itemsLayout, parent, false)
+ return ViewHolder(view, listener)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val app = dataSet[position]
+ holder.bind(app)
+ }
+
+ override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt b/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt
new file mode 100644
index 0000000..d73f770
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.common
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import foundation.e.advancedprivacy.Notifications
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+
+class BootCompletedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
+ val localStateRepository = LocalStateRepository(context)
+ if (localStateRepository.firstBoot) {
+ Notifications.showFirstBootNotification(context)
+ localStateRepository.firstBoot = false
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt b/app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt
new file mode 100644
index 0000000..3af0b37
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.common
+
+// Definition of a Factory interface with a function to create objects of a type
+interface Factory<T> {
+ fun create(): T
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt b/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt
new file mode 100644
index 0000000..ca4fcb6
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt
@@ -0,0 +1,333 @@
+/*
+ * 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.common
+
+import android.content.Context
+import android.graphics.Canvas
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.style.DynamicDrawableSpan
+import android.text.style.ImageSpan
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.text.toSpannable
+import androidx.core.view.isVisible
+import com.github.mikephil.charting.charts.BarChart
+import com.github.mikephil.charting.components.AxisBase
+import com.github.mikephil.charting.components.MarkerView
+import com.github.mikephil.charting.components.XAxis
+import com.github.mikephil.charting.components.YAxis
+import com.github.mikephil.charting.components.YAxis.AxisDependency
+import com.github.mikephil.charting.data.BarData
+import com.github.mikephil.charting.data.BarDataSet
+import com.github.mikephil.charting.data.BarEntry
+import com.github.mikephil.charting.data.Entry
+import com.github.mikephil.charting.formatter.ValueFormatter
+import com.github.mikephil.charting.highlight.Highlight
+import com.github.mikephil.charting.listener.OnChartValueSelectedListener
+import com.github.mikephil.charting.renderer.XAxisRenderer
+import com.github.mikephil.charting.utils.MPPointF
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.extensions.dpToPxF
+import kotlin.math.floor
+
+class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbove: Boolean = true) {
+ var data = emptyList<Pair<Int, Int>>()
+ set(value) {
+ field = value
+ refreshDataSet()
+ }
+ var labels = emptyList<String>()
+
+ var graduations: List<String?>? = null
+
+ private var isHighlighted = false
+
+ init {
+ barChart.description = null
+ barChart.setTouchEnabled(true)
+ barChart.setScaleEnabled(false)
+
+ barChart.setDrawGridBackground(false)
+ barChart.setDrawBorders(false)
+ barChart.axisLeft.isEnabled = false
+ barChart.axisRight.isEnabled = false
+
+ barChart.legend.isEnabled = false
+
+ if (isMarkerAbove) prepareXAxisDashboardDay() else prepareXAxisMarkersBelow()
+
+ val periodMarker = PeriodMarkerView(context, isMarkerAbove)
+ periodMarker.chartView = barChart
+ barChart.marker = periodMarker
+
+ barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
+ override fun onValueSelected(e: Entry?, h: Highlight?) {
+ h?.let {
+ val index = it.x.toInt()
+ if (index >= 0 &&
+ index < labels.size &&
+ index < this@GraphHolder.data.size
+ ) {
+ val period = labels[index]
+ val (blocked, leaked) = this@GraphHolder.data[index]
+ periodMarker.setLabel(period, blocked, leaked)
+ }
+ }
+ isHighlighted = true
+ }
+
+ override fun onNothingSelected() {
+ isHighlighted = false
+ }
+ })
+ }
+
+ private fun prepareXAxisDashboardDay() {
+ barChart.extraTopOffset = 44f
+
+ barChart.offsetTopAndBottom(0)
+
+ barChart.setXAxisRenderer(object : XAxisRenderer(barChart.viewPortHandler, barChart.xAxis, barChart.getTransformer(AxisDependency.LEFT)) {
+ override fun renderAxisLine(c: Canvas) {
+ mAxisLinePaint.color = mXAxis.axisLineColor
+ mAxisLinePaint.strokeWidth = mXAxis.axisLineWidth
+ mAxisLinePaint.pathEffect = mXAxis.axisLineDashPathEffect
+
+ // Top line
+ c.drawLine(
+ mViewPortHandler.contentLeft(),
+ mViewPortHandler.contentTop(), mViewPortHandler.contentRight(),
+ mViewPortHandler.contentTop(), mAxisLinePaint
+ )
+
+ // Bottom line
+ c.drawLine(
+ mViewPortHandler.contentLeft(),
+ mViewPortHandler.contentBottom() - 7.dpToPxF(context),
+ mViewPortHandler.contentRight(),
+ mViewPortHandler.contentBottom() - 7.dpToPxF(context),
+ mAxisLinePaint
+ )
+ }
+
+ override fun renderGridLines(c: Canvas) {
+ if (!mXAxis.isDrawGridLinesEnabled || !mXAxis.isEnabled) return
+ val clipRestoreCount = c.save()
+ c.clipRect(gridClippingRect)
+ if (mRenderGridLinesBuffer.size != mAxis.mEntryCount * 2) {
+ mRenderGridLinesBuffer = FloatArray(mXAxis.mEntryCount * 2)
+ }
+ val positions = mRenderGridLinesBuffer
+ run {
+ var i = 0
+ while (i < positions.size) {
+ positions[i] = mXAxis.mEntries[i / 2]
+ positions[i + 1] = mXAxis.mEntries[i / 2]
+ i += 2
+ }
+ }
+
+ mTrans.pointValuesToPixel(positions)
+ setupGridPaint()
+ val gridLinePath = mRenderGridLinesPath
+ gridLinePath.reset()
+ var i = 0
+ while (i < positions.size) {
+ val bottomY = if (graduations?.getOrNull(i / 2) != null) 0 else 3
+ val x = positions[i]
+ gridLinePath.moveTo(x, mViewPortHandler.contentBottom() - 7.dpToPxF(context))
+ gridLinePath.lineTo(x, mViewPortHandler.contentBottom() - bottomY.dpToPxF(context))
+
+ c.drawPath(gridLinePath, mGridPaint)
+
+ gridLinePath.reset()
+
+ i += 2
+ }
+ c.restoreToCount(clipRestoreCount)
+ }
+ })
+
+ barChart.setDrawValueAboveBar(false)
+ barChart.xAxis.apply {
+ isEnabled = true
+ position = XAxis.XAxisPosition.BOTTOM
+
+ setDrawGridLines(true)
+ setDrawLabels(true)
+ setCenterAxisLabels(false)
+ setLabelCount(25, true)
+ textColor = context.getColor(R.color.primary_text)
+ valueFormatter = object : ValueFormatter() {
+ override fun getAxisLabel(value: Float, axis: AxisBase?): String {
+ return graduations?.getOrNull(floor(value).toInt() + 1) ?: ""
+ }
+ }
+ }
+ }
+
+ private fun prepareXAxisMarkersBelow() {
+ barChart.extraBottomOffset = 44f
+
+ barChart.offsetTopAndBottom(0)
+ barChart.setDrawValueAboveBar(false)
+
+ barChart.xAxis.apply {
+ isEnabled = true
+ position = XAxis.XAxisPosition.BOTH_SIDED
+ setDrawGridLines(false)
+ setDrawLabels(false)
+ }
+ }
+
+ fun highlightIndex(index: Int) {
+ if (index >= 0 && index < data.size) {
+ val xPx = barChart.getTransformer(YAxis.AxisDependency.LEFT)
+ .getPixelForValues(index.toFloat(), 0f)
+ .x
+ val highlight = Highlight(
+ index.toFloat(), 0f,
+ xPx.toFloat(), 0f,
+ 0, YAxis.AxisDependency.LEFT
+ )
+
+ barChart.highlightValue(highlight, true)
+ }
+ }
+
+ private fun refreshDataSet() {
+ val trackersDataSet = BarDataSet(
+ data.mapIndexed { index, value ->
+ BarEntry(
+ index.toFloat(),
+ floatArrayOf(value.first.toFloat(), value.second.toFloat())
+ )
+ },
+ ""
+ ).apply {
+
+ val blockedColor = ContextCompat.getColor(context, R.color.accent)
+ val leakedColor = ContextCompat.getColor(context, R.color.red_off)
+
+ colors = listOf(
+ blockedColor,
+ leakedColor
+ )
+
+ setDrawValues(false)
+ }
+
+ barChart.data = BarData(trackersDataSet)
+ barChart.invalidate()
+ }
+}
+
+class PeriodMarkerView(context: Context, private val isMarkerAbove: Boolean = true) : MarkerView(context, R.layout.chart_tooltip) {
+ enum class ArrowPosition { LEFT, CENTER, RIGHT }
+
+ private val arrowMargins = 10.dpToPxF(context)
+ private val mOffset2 = MPPointF(0f, 0f)
+
+ private fun getArrowPosition(posX: Float): ArrowPosition {
+ val halfWidth = width / 2
+
+ return chartView?.let { chart ->
+ if (posX < halfWidth) {
+ ArrowPosition.LEFT
+ } else if (chart.width - posX < halfWidth) {
+ ArrowPosition.RIGHT
+ } else {
+ ArrowPosition.CENTER
+ }
+ } ?: ArrowPosition.CENTER
+ }
+
+ private fun showArrow(position: ArrowPosition?) {
+ val ids = listOf(
+ R.id.arrow_top_left, R.id.arrow_top_center, R.id.arrow_top_right,
+ R.id.arrow_bottom_left, R.id.arrow_bottom_center, R.id.arrow_bottom_right
+ )
+
+ val toShow = if (isMarkerAbove) when (position) {
+ ArrowPosition.LEFT -> R.id.arrow_bottom_left
+ ArrowPosition.CENTER -> R.id.arrow_bottom_center
+ ArrowPosition.RIGHT -> R.id.arrow_bottom_right
+ else -> null
+ } else when (position) {
+ ArrowPosition.LEFT -> R.id.arrow_top_left
+ ArrowPosition.CENTER -> R.id.arrow_top_center
+ ArrowPosition.RIGHT -> R.id.arrow_top_right
+ else -> null
+ }
+
+ ids.forEach { id ->
+ val showIt = id == toShow
+ findViewById<View>(id)?.let {
+ if (it.isVisible != showIt) {
+ it.isVisible = showIt
+ }
+ }
+ }
+ }
+
+ fun setLabel(period: String, blocked: Int, leaked: Int) {
+ val span = SpannableStringBuilder(period)
+ span.append(": $blocked ")
+ span.setSpan(
+ ImageSpan(context, R.drawable.ic_legend_blocked, DynamicDrawableSpan.ALIGN_BASELINE),
+ span.length - 1,
+ span.length,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ span.append(" $leaked ")
+ span.setSpan(
+ ImageSpan(context, R.drawable.ic_legend_leaked, DynamicDrawableSpan.ALIGN_BASELINE),
+ span.length - 1,
+ span.length,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ findViewById<TextView>(R.id.label).text = span.toSpannable()
+ }
+
+ override fun refreshContent(e: Entry?, highlight: Highlight?) {
+ highlight?.let {
+ showArrow(getArrowPosition(highlight.xPx))
+ }
+ super.refreshContent(e, highlight)
+ }
+
+ override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF {
+ val x = when (getArrowPosition(posX)) {
+ ArrowPosition.LEFT -> -arrowMargins
+ ArrowPosition.RIGHT -> -width + arrowMargins
+ ArrowPosition.CENTER -> -width.toFloat() / 2
+ }
+
+ mOffset2.x = x
+ mOffset2.y = if (isMarkerAbove) -posY
+ else -posY + (chartView?.height?.toFloat() ?: 0f) - height
+
+ return mOffset2
+ }
+
+ override fun draw(canvas: Canvas?, posX: Float, posY: Float) {
+ super.draw(canvas, posX, posY)
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt
new file mode 100644
index 0000000..1417977
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.common
+
+import androidx.annotation.LayoutRes
+import com.google.android.material.appbar.MaterialToolbar
+
+abstract class NavToolbarFragment(@LayoutRes contentLayoutId: Int) : ToolbarFragment(contentLayoutId) {
+
+ override fun setupToolbar(toolbar: MaterialToolbar) {
+ super.setupToolbar(toolbar)
+ toolbar.apply {
+ setNavigationOnClickListener {
+ requireActivity().onBackPressed()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt b/app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt
new file mode 100644
index 0000000..c10d755
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.common
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.RadioButton
+
+/**
+ * A custom [RadioButton] which displays the radio drawable on the right side.
+ */
+@SuppressLint("AppCompatCustomView")
+class RightRadioButton : RadioButton {
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
+ context,
+ attrs,
+ defStyleAttr
+ )
+
+ // Returns layout direction as right-to-left to draw the compound button on right side.
+ override fun getLayoutDirection(): Int {
+ return LAYOUT_DIRECTION_RTL
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt b/app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt
new file mode 100644
index 0000000..f87834a
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.common
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.DynamicDrawableSpan
+import android.text.style.ImageSpan
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.appcompat.widget.TooltipCompat
+import foundation.e.advancedprivacy.R
+
+fun setToolTipForAsterisk(
+ textView: TextView,
+ @StringRes textId: Int,
+ @StringRes tooltipTextId: Int
+) {
+ textView.text = asteriskAsInfoIconSpannable(textView.context, textId, textView.textColors)
+ TooltipCompat.setTooltipText(textView, textView.context.getString(tooltipTextId))
+
+ textView.setOnClickListener { it.performLongClick() }
+}
+
+private fun asteriskAsInfoIconSpannable(
+ context: Context,
+ @StringRes textId: Int,
+ tint: ColorStateList
+): Spannable {
+ val spannable = SpannableString(context.getString(textId))
+ val index = spannable.lastIndexOf("*")
+ if (index != -1) {
+ AppCompatResources.getDrawable(context, R.drawable.ic_info_16dp)?.let {
+ it.setTintList(tint)
+ it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight)
+ spannable.setSpan(
+ ImageSpan(it, DynamicDrawableSpan.ALIGN_CENTER),
+ index,
+ index + 1,
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE
+ )
+ }
+ }
+ return spannable
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt b/app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt
new file mode 100644
index 0000000..e9ec060
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.common
+
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlin.time.Duration
+
+@FlowPreview
+fun <T> Flow<T>.throttleFirst(windowDuration: Duration): Flow<T> = flow {
+ var lastEmissionTime = 0L
+ collect { upstream ->
+ val currentTime = System.currentTimeMillis()
+ val mayEmit = currentTime - lastEmissionTime > windowDuration.inWholeMilliseconds
+ if (mayEmit) {
+ lastEmissionTime = currentTime
+ emit(upstream)
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt
new file mode 100644
index 0000000..d8ee8ea
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.common
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+
+class ToggleAppsAdapter(
+ private val itemsLayout: Int,
+ private val listener: (String) -> Unit
+) :
+ RecyclerView.Adapter<ToggleAppsAdapter.ViewHolder>() {
+
+ class ViewHolder(view: View, private val listener: (String) -> Unit) : RecyclerView.ViewHolder(view) {
+ val appName: TextView = view.findViewById(R.id.title)
+
+ val togglePermission: CheckBox = view.findViewById(R.id.toggle)
+
+ fun bind(item: Pair<ApplicationDescription, Boolean>, isEnabled: Boolean) {
+ appName.text = item.first.label
+ togglePermission.isChecked = item.second
+ togglePermission.isEnabled = isEnabled
+
+ itemView.findViewById<ImageView>(R.id.icon).setImageDrawable(item.first.icon)
+ togglePermission.setOnClickListener { listener(item.first.packageName) }
+ }
+ }
+
+ var dataSet: List<Pair<ApplicationDescription, Boolean>> = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ var isEnabled: Boolean = true
+
+ fun setData(list: List<Pair<ApplicationDescription, Boolean>>, isEnabled: Boolean = true) {
+ this.isEnabled = isEnabled
+ dataSet = list
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(itemsLayout, parent, false)
+ return ViewHolder(view, listener)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val permission = dataSet[position]
+ holder.bind(permission, isEnabled)
+ }
+
+ override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt
new file mode 100644
index 0000000..fb3ea14
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.common
+
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.LayoutRes
+import androidx.fragment.app.Fragment
+import com.google.android.material.appbar.MaterialToolbar
+import foundation.e.advancedprivacy.R
+
+abstract class ToolbarFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId) {
+
+ /**
+ * @return title to be used in toolbar
+ */
+ abstract fun getTitle(): String
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupToolbar(view.findViewById(R.id.toolbar))
+ }
+
+ open fun setupToolbar(toolbar: MaterialToolbar) {
+ toolbar.title = getTitle()
+ }
+
+ fun getToolbar(): MaterialToolbar? = view?.findViewById(R.id.toolbar)
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt b/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt
new file mode 100644
index 0000000..98deeb1
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.common
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.widget.CheckBox
+import androidx.appcompat.app.AlertDialog
+import foundation.e.advancedprivacy.AdvancedPrivacyApplication
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.domain.entities.MainFeatures
+import foundation.e.advancedprivacy.domain.entities.MainFeatures.FAKE_LOCATION
+import foundation.e.advancedprivacy.domain.entities.MainFeatures.IP_SCRAMBLING
+import foundation.e.advancedprivacy.domain.entities.MainFeatures.TRACKERS_CONTROL
+import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase
+import foundation.e.advancedprivacy.main.MainActivity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+
+class WarningDialog : Activity() {
+ companion object {
+ private const val PARAM_FEATURE = "feature"
+
+ fun startListening(
+ showFeaturesWarningUseCase: ShowFeaturesWarningUseCase,
+ appScope: CoroutineScope,
+ appContext: Context
+ ) {
+ showFeaturesWarningUseCase.showWarning().map { feature ->
+ appContext.startActivity(
+ createIntent(context = appContext, feature = feature)
+ )
+ }.launchIn(appScope)
+ }
+
+ private fun createIntent(
+ context: Context,
+ feature: MainFeatures,
+ ): Intent {
+ val intent = Intent(context, WarningDialog::class.java)
+ intent.putExtra(PARAM_FEATURE, feature.name)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ return intent
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ getWindow().setBackgroundDrawable(ColorDrawable(0))
+
+ val feature = try {
+ MainFeatures.valueOf(intent.getStringExtra(PARAM_FEATURE) ?: "")
+ } catch (e: Exception) {
+ Log.e("WarningDialog", "Missing mandatory activity parameter", e)
+ finish()
+ return
+ }
+
+ showWarningDialog(feature)
+ }
+
+ private fun showWarningDialog(feature: MainFeatures) {
+ val builder = AlertDialog.Builder(this)
+ builder.setOnDismissListener { finish() }
+
+ val content: View = layoutInflater.inflate(R.layout.alertdialog_do_not_show_again, null)
+ val checkbox = content.findViewById<CheckBox>(R.id.checkbox)
+ builder.setView(content)
+
+ builder.setMessage(
+ when (feature) {
+ TRACKERS_CONTROL -> R.string.warningdialog_trackers_message
+ FAKE_LOCATION -> R.string.warningdialog_location_message
+ IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_message
+ }
+ )
+
+ builder.setTitle(
+ when (feature) {
+ TRACKERS_CONTROL -> R.string.warningdialog_trackers_title
+ FAKE_LOCATION -> R.string.warningdialog_location_title
+ IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_title
+ }
+ )
+
+ builder.setPositiveButton(
+ when (feature) {
+ IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_cta
+ else -> R.string.ok
+ }
+ ) { _, _ ->
+ if (checkbox.isChecked()) {
+ (application as AdvancedPrivacyApplication)
+ .dependencyContainer.showFeaturesWarningUseCase
+ .doNotShowAgain(feature)
+ }
+ finish()
+ }
+
+ if (feature == TRACKERS_CONTROL) {
+ builder.setNeutralButton(R.string.warningdialog_trackers_secondary_cta) { _, _ ->
+ startActivity(MainActivity.createTrackersIntent(this))
+ finish()
+ }
+ }
+
+ builder.show()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt b/app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt
new file mode 100644
index 0000000..652aefd
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.common.extensions
+
+import android.content.Context
+
+fun Int.dpToPxF(context: Context): Float = this.toFloat() * context.resources.displayMetrics.density
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..568d76b
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt
@@ -0,0 +1,133 @@
+/*
+ * 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 com.google.gson.Gson
+import foundation.e.privacymodules.trackers.api.Tracker
+import retrofit2.Retrofit
+import retrofit2.converter.scalars.ScalarsConverterFactory
+import retrofit2.http.GET
+import timber.log.Timber
+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()
+ try {
+ saveData(eTrackerFile, api.trackers())
+ initTrackersFile()
+ } catch (e: Exception) {
+ Timber.e("While updating trackers", e)
+ }
+ }
+
+ 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) {
+ Timber.e("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?
+ )
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/AppWithCounts.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/AppWithCounts.kt
new file mode 100644
index 0000000..4169ecc
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/AppWithCounts.kt
@@ -0,0 +1,59 @@
+/*
+ * 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
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+
+data class AppWithCounts(
+ val appDesc: ApplicationDescription,
+ val packageName: String,
+ val uid: Int,
+ var label: CharSequence?,
+ var icon: Drawable?,
+ val isWhitelisted: Boolean = false,
+ val trackersCount: Int = 0,
+ val whiteListedTrackersCount: Int = 0,
+ val blockedLeaks: Int = 0,
+ val leaks: Int = 0,
+) {
+ constructor(
+ app: ApplicationDescription,
+ isWhitelisted: Boolean,
+ trackersCount: Int,
+ whiteListedTrackersCount: Int,
+ blockedLeaks: Int,
+ leaks: Int,
+ ) :
+ this(
+ appDesc = app,
+ packageName = app.packageName,
+ uid = app.uid,
+ label = app.label,
+ icon = app.icon,
+ isWhitelisted = isWhitelisted,
+ trackersCount = trackersCount,
+ whiteListedTrackersCount = whiteListedTrackersCount,
+ blockedLeaks = blockedLeaks,
+ leaks = leaks
+ )
+
+ val blockedTrackersCount get() = if (isWhitelisted) 0
+ else Math.max(trackersCount - whiteListedTrackersCount, 0)
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt
new file mode 100644
index 0000000..986e798
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.domain.entities
+
+enum class InternetPrivacyMode {
+ REAL_IP,
+ HIDE_IP,
+ HIDE_IP_LOADING,
+ REAL_IP_LOADING;
+
+ val isChecked get() = this == HIDE_IP || this == HIDE_IP_LOADING
+
+ val isLoading get() = this == HIDE_IP_LOADING || this == REAL_IP_LOADING
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt
new file mode 100644
index 0000000..62581eb
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.domain.entities
+
+enum class LocationMode {
+ REAL_LOCATION, RANDOM_LOCATION, SPECIFIC_LOCATION
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/MainFeatures.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/MainFeatures.kt
new file mode 100644
index 0000000..c63d3ab
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/MainFeatures.kt
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+enum class MainFeatures {
+ TRACKERS_CONTROL, FAKE_LOCATION, IP_SCRAMBLING
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/QuickPrivacyState.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/QuickPrivacyState.kt
new file mode 100644
index 0000000..c21bb1d
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/QuickPrivacyState.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+enum class QuickPrivacyState {
+ DISABLED, ENABLED, FULL_ENABLED;
+
+ fun isEnabled(): Boolean = this != DISABLED
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackerMode.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackerMode.kt
new file mode 100644
index 0000000..2033251
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackerMode.kt
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+enum class TrackerMode {
+ DENIED, CUSTOM, VULNERABLE
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt
new file mode 100644
index 0000000..c0fa637
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 TrackersPeriodicStatistics(
+ val callsBlockedNLeaked: List<Pair<Int, Int>>,
+ val periods: List<String>,
+ val trackersCount: Int,
+ val graduations: List<String?>? = null
+)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppListUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppListUseCase.kt
new file mode 100644
index 0000000..8d38ee8
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppListUseCase.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.domain.usecases
+
+import foundation.e.advancedprivacy.data.repositories.AppListsRepository
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import kotlinx.coroutines.flow.Flow
+
+class AppListUseCase(
+ private val appListsRepository: AppListsRepository
+) {
+ val dummySystemApp = appListsRepository.dummySystemApp
+ fun getApp(uid: Int): ApplicationDescription {
+ return when (uid) {
+ dummySystemApp.uid -> dummySystemApp
+ appListsRepository.dummyCompatibilityApp.uid ->
+ appListsRepository.dummyCompatibilityApp
+ else -> appListsRepository.getApp(uid) ?: dummySystemApp
+ }
+ }
+ fun getAppsUsingInternet(): Flow<List<ApplicationDescription>> {
+ return appListsRepository.mainProfileApps()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt
new file mode 100644
index 0000000..9b99b95
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt
@@ -0,0 +1,209 @@
+/*
+ * 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.domain.usecases
+
+import android.app.AppOpsManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.os.Bundle
+import android.util.Log
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+import foundation.e.advancedprivacy.domain.entities.LocationMode
+import foundation.e.advancedprivacy.dummy.CityDataSource
+import foundation.e.privacymodules.fakelocation.IFakeLocationModule
+import foundation.e.privacymodules.permissions.PermissionsPrivacyModule
+import foundation.e.privacymodules.permissions.data.AppOpModes
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlin.random.Random
+
+class FakeLocationStateUseCase(
+ private val fakeLocationModule: IFakeLocationModule,
+ private val permissionsModule: PermissionsPrivacyModule,
+ private val localStateRepository: LocalStateRepository,
+ private val citiesRepository: CityDataSource,
+ private val appDesc: ApplicationDescription,
+ private val appContext: Context,
+ coroutineScope: CoroutineScope
+) {
+ companion object {
+ private const val TAG = "FakeLocationStateUseCase"
+ }
+
+ private val _configuredLocationMode = MutableStateFlow<Triple<LocationMode, Float?, Float?>>(Triple(LocationMode.REAL_LOCATION, null, null))
+ val configuredLocationMode: StateFlow<Triple<LocationMode, Float?, Float?>> = _configuredLocationMode
+
+ init {
+ coroutineScope.launch {
+ localStateRepository.fakeLocationEnabled.collect {
+ applySettings(it, localStateRepository.fakeLocation)
+ }
+ }
+ }
+
+ private val locationManager: LocationManager
+ get() = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+
+ private fun hasAcquireLocationPermission(): Boolean {
+ return (appContext.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) ||
+ permissionsModule.toggleDangerousPermission(appDesc, android.Manifest.permission.ACCESS_FINE_LOCATION, true)
+ }
+
+ private fun applySettings(isEnabled: Boolean, fakeLocation: Pair<Float, Float>, isSpecificLocation: Boolean = false) {
+ _configuredLocationMode.value = computeLocationMode(isEnabled, fakeLocation, isSpecificLocation)
+
+ if (isEnabled && hasAcquireMockLocationPermission()) {
+ fakeLocationModule.startFakeLocation()
+ fakeLocationModule.setFakeLocation(fakeLocation.first.toDouble(), fakeLocation.second.toDouble())
+ localStateRepository.locationMode.value = configuredLocationMode.value.first
+ } else {
+ fakeLocationModule.stopFakeLocation()
+ localStateRepository.locationMode.value = LocationMode.REAL_LOCATION
+ }
+ }
+
+ private fun hasAcquireMockLocationPermission(): Boolean {
+ return (permissionsModule.getAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION) == AppOpModes.ALLOWED) ||
+ permissionsModule.setAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpModes.ALLOWED)
+ }
+
+ fun setSpecificLocation(latitude: Float, longitude: Float) {
+ setFakeLocation(latitude to longitude, true)
+ }
+
+ fun setRandomLocation() {
+ val randomIndex = Random.nextInt(citiesRepository.citiesLocationsList.size)
+ val location = citiesRepository.citiesLocationsList[randomIndex]
+
+ setFakeLocation(location)
+ }
+
+ private fun setFakeLocation(location: Pair<Float, Float>, isSpecificLocation: Boolean = false) {
+ localStateRepository.fakeLocation = location
+ localStateRepository.setFakeLocationEnabled(true)
+ applySettings(true, location, isSpecificLocation)
+ }
+
+ fun stopFakeLocation() {
+ localStateRepository.setFakeLocationEnabled(false)
+ applySettings(false, localStateRepository.fakeLocation)
+ }
+
+ private fun computeLocationMode(
+ isFakeLocationEnabled: Boolean,
+ fakeLocation: Pair<Float, Float>,
+ isSpecificLocation: Boolean = false,
+ ): Triple<LocationMode, Float?, Float?> {
+ return Triple(
+ when {
+ !isFakeLocationEnabled -> LocationMode.REAL_LOCATION
+ (fakeLocation in citiesRepository.citiesLocationsList && !isSpecificLocation) ->
+ LocationMode.RANDOM_LOCATION
+ else -> LocationMode.SPECIFIC_LOCATION
+ },
+ fakeLocation.first,
+ fakeLocation.second
+ )
+ }
+
+ val currentLocation = MutableStateFlow<Location?>(null)
+
+ private var localListener = object : LocationListener {
+
+ override fun onLocationChanged(location: Location) {
+ currentLocation.update { previous ->
+ if ((previous?.time ?: 0) + 1800 < location.time ||
+ (previous?.accuracy ?: Float.MAX_VALUE) > location.accuracy
+ ) {
+ location
+ } else {
+ previous
+ }
+ }
+ }
+
+ // Deprecated since API 29, never called.
+ override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
+
+ override fun onProviderEnabled(provider: String) {
+ reset()
+ }
+
+ override fun onProviderDisabled(provider: String) {
+ reset()
+ }
+
+ private fun reset() {
+ stopListeningLocation()
+ currentLocation.value = null
+ startListeningLocation()
+ }
+ }
+
+ fun startListeningLocation(): Boolean {
+ return if (hasAcquireLocationPermission()) {
+ requestLocationUpdates()
+ true
+ } else false
+ }
+
+ fun stopListeningLocation() {
+ locationManager.removeUpdates(localListener)
+ }
+
+ private fun requestLocationUpdates() {
+ val networkProvider = LocationManager.NETWORK_PROVIDER
+ .takeIf { it in locationManager.allProviders }
+ val gpsProvider = LocationManager.GPS_PROVIDER
+ .takeIf { it in locationManager.allProviders }
+
+ try {
+ networkProvider?.let {
+ locationManager.requestLocationUpdates(
+ it,
+ 1000L,
+ 0f,
+ localListener
+ )
+ }
+ gpsProvider?.let {
+ locationManager.requestLocationUpdates(
+ it,
+ 1000L,
+ 0f,
+ localListener
+ )
+ }
+
+ networkProvider?.let { locationManager.getLastKnownLocation(it) }
+ ?: gpsProvider?.let { locationManager.getLastKnownLocation(it) }
+ ?.let {
+ localListener.onLocationChanged(it)
+ }
+ } catch (se: SecurityException) {
+ Log.e(TAG, "Missing permission", se)
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt
new file mode 100644
index 0000000..475c05d
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.domain.usecases
+
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import foundation.e.advancedprivacy.domain.entities.LocationMode
+import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState
+import foundation.e.advancedprivacy.domain.entities.TrackerMode
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+class GetQuickPrivacyStateUseCase(
+ private val localStateRepository: LocalStateRepository
+) {
+ val quickPrivacyState: Flow<QuickPrivacyState> = combine(
+ localStateRepository.blockTrackers,
+ localStateRepository.areAllTrackersBlocked,
+ localStateRepository.locationMode,
+ localStateRepository.internetPrivacyMode
+ ) { isBlockTrackers, isAllTrackersBlocked, locationMode, internetPrivacyMode ->
+ when {
+ !isBlockTrackers &&
+ locationMode == LocationMode.REAL_LOCATION &&
+ internetPrivacyMode == InternetPrivacyMode.REAL_IP -> QuickPrivacyState.DISABLED
+
+ isAllTrackersBlocked &&
+ locationMode != LocationMode.REAL_LOCATION &&
+ internetPrivacyMode in listOf(
+ InternetPrivacyMode.HIDE_IP,
+ InternetPrivacyMode.HIDE_IP_LOADING
+ ) -> QuickPrivacyState.FULL_ENABLED
+
+ else -> QuickPrivacyState.ENABLED
+ }
+ }
+
+ val trackerMode: Flow<TrackerMode> = combine(
+ localStateRepository.blockTrackers,
+ localStateRepository.areAllTrackersBlocked
+ ) { isBlockTrackers, isAllTrackersBlocked ->
+ when {
+ isBlockTrackers && isAllTrackersBlocked -> TrackerMode.DENIED
+ isBlockTrackers && !isAllTrackersBlocked -> TrackerMode.CUSTOM
+ else -> TrackerMode.VULNERABLE
+ }
+ }
+
+ val isLocationHidden: Flow<Boolean> = localStateRepository.locationMode.map { locationMode ->
+ locationMode != LocationMode.REAL_LOCATION
+ }
+
+ val locationMode: StateFlow<LocationMode> = localStateRepository.locationMode
+
+ val ipScramblingMode: Flow<InternetPrivacyMode> = localStateRepository.internetPrivacyMode
+
+ fun toggleTrackers() {
+ localStateRepository.setBlockTrackers(!localStateRepository.blockTrackers.value)
+ }
+
+ fun toggleLocation() {
+ localStateRepository.setFakeLocationEnabled(!localStateRepository.fakeLocationEnabled.value)
+ }
+
+ fun toggleIpScrambling() {
+ localStateRepository.setIpScramblingSetting(!localStateRepository.ipScramblingSetting.value)
+ }
+
+ val otherVpnRunning: SharedFlow<ApplicationDescription> = localStateRepository.otherVpnRunning
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt
new file mode 100644
index 0000000..8c94602
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION, 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.domain.usecases
+
+import foundation.e.advancedprivacy.data.repositories.AppListsRepository
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode.HIDE_IP
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode.HIDE_IP_LOADING
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode.REAL_IP
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode.REAL_IP_LOADING
+import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
+import foundation.e.privacymodules.permissions.IPermissionsPrivacyModule
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class IpScramblingStateUseCase(
+ private val ipScramblerModule: IIpScramblerModule,
+ private val permissionsPrivacyModule: IPermissionsPrivacyModule,
+ private val appDesc: ApplicationDescription,
+ private val localStateRepository: LocalStateRepository,
+ private val appListsRepository: AppListsRepository,
+ private val coroutineScope: CoroutineScope
+) {
+ val internetPrivacyMode: StateFlow<InternetPrivacyMode> = callbackFlow {
+ val listener = object : IIpScramblerModule.Listener {
+ override fun onStatusChanged(newStatus: IIpScramblerModule.Status) {
+ trySend(map(newStatus))
+ }
+
+ override fun log(message: String) {}
+ override fun onTrafficUpdate(
+ upload: Long,
+ download: Long,
+ read: Long,
+ write: Long
+ ) {
+ }
+ }
+ ipScramblerModule.addListener(listener)
+ ipScramblerModule.requestStatus()
+ awaitClose { ipScramblerModule.removeListener(listener) }
+ }.stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.Eagerly,
+ initialValue = REAL_IP
+ )
+
+ init {
+ coroutineScope.launch(Dispatchers.Default) {
+ localStateRepository.ipScramblingSetting.collect {
+ applySettings(it)
+ }
+ }
+
+ coroutineScope.launch {
+ internetPrivacyMode.collect { localStateRepository.internetPrivacyMode.value = it }
+ }
+ }
+
+ fun toggle(hideIp: Boolean) {
+ localStateRepository.setIpScramblingSetting(enabled = hideIp)
+ }
+
+ private fun getHiddenPackageNames(): List<String> {
+ return appListsRepository.getMainProfileHiddenSystemApps().map { it.packageName }
+ }
+
+ val bypassTorApps: Set<String> get() {
+ var whitelist = ipScramblerModule.appList
+ if (getHiddenPackageNames().any { it in whitelist }) {
+ val mutable = whitelist.toMutableSet()
+ mutable.removeAll(getHiddenPackageNames())
+ mutable.add(appListsRepository.dummySystemApp.packageName)
+ whitelist = mutable
+ }
+ if (AppListsRepository.compatibiltyPNames.any { it in whitelist }) {
+ val mutable = whitelist.toMutableSet()
+ mutable.removeAll(AppListsRepository.compatibiltyPNames)
+ mutable.add(appListsRepository.dummyCompatibilityApp.packageName)
+ whitelist = mutable
+ }
+ return whitelist
+ }
+
+ fun toggleBypassTor(packageName: String) {
+ val visibleList = bypassTorApps.toMutableSet()
+ val rawList = ipScramblerModule.appList.toMutableSet()
+
+ if (visibleList.contains(packageName)) {
+ if (packageName == appListsRepository.dummySystemApp.packageName) {
+ rawList.removeAll(getHiddenPackageNames())
+ } else if (packageName == appListsRepository.dummyCompatibilityApp.packageName) {
+ rawList.removeAll(AppListsRepository.compatibiltyPNames)
+ } else {
+ rawList.remove(packageName)
+ }
+ } else {
+ if (packageName == appListsRepository.dummySystemApp.packageName) {
+ rawList.addAll(getHiddenPackageNames())
+ } else if (packageName == appListsRepository.dummyCompatibilityApp.packageName) {
+ rawList.addAll(AppListsRepository.compatibiltyPNames)
+ } else {
+ rawList.add(packageName)
+ }
+ }
+ ipScramblerModule.appList = rawList
+ }
+
+ private fun applySettings(isIpScramblingEnabled: Boolean) {
+ val currentMode = localStateRepository.internetPrivacyMode.value
+ when {
+ isIpScramblingEnabled && currentMode in setOf(REAL_IP, REAL_IP_LOADING) ->
+ applyStartIpScrambling()
+
+ !isIpScramblingEnabled && currentMode in setOf(HIDE_IP, HIDE_IP_LOADING) ->
+ ipScramblerModule.stop()
+
+ else -> {}
+ }
+ }
+
+ private fun applyStartIpScrambling() {
+ ipScramblerModule.prepareAndroidVpn()?.let {
+ permissionsPrivacyModule.setVpnPackageAuthorization(appDesc.packageName)
+ permissionsPrivacyModule.getAlwaysOnVpnPackage()
+ }?.let {
+ coroutineScope.launch {
+ localStateRepository.emitOtherVpnRunning(
+ permissionsPrivacyModule.getApplicationDescription(packageName = it, withIcon = false)
+ )
+ }
+ localStateRepository.setIpScramblingSetting(enabled = false)
+ } ?: run {
+ ipScramblerModule.start(enableNotification = false)
+ }
+ }
+
+ private fun map(status: IIpScramblerModule.Status): InternetPrivacyMode {
+ return when (status) {
+ IIpScramblerModule.Status.OFF -> REAL_IP
+ IIpScramblerModule.Status.ON -> HIDE_IP
+ IIpScramblerModule.Status.STARTING -> HIDE_IP_LOADING
+ IIpScramblerModule.Status.STOPPING,
+ IIpScramblerModule.Status.START_DISABLED -> REAL_IP_LOADING
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt
new file mode 100644
index 0000000..11bce86
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.usecases
+
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+import foundation.e.advancedprivacy.domain.entities.MainFeatures
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+class ShowFeaturesWarningUseCase(
+ private val localStateRepository: LocalStateRepository
+) {
+
+ fun showWarning(): Flow<MainFeatures> {
+ return merge(
+ localStateRepository.blockTrackers.drop(1).dropWhile { !it }
+ .filter { it && !localStateRepository.hideWarningTrackers }
+ .map { MainFeatures.TRACKERS_CONTROL },
+ localStateRepository.fakeLocationEnabled.drop(1).dropWhile { !it }
+ .filter { it && !localStateRepository.hideWarningLocation }
+ .map { MainFeatures.FAKE_LOCATION },
+ localStateRepository.ipScramblingSetting.drop(1).dropWhile { !it }
+ .filter { it && !localStateRepository.hideWarningIpScrambling }
+ .map { MainFeatures.IP_SCRAMBLING }
+ )
+ }
+
+ fun doNotShowAgain(feature: MainFeatures) {
+ when (feature) {
+ MainFeatures.TRACKERS_CONTROL -> localStateRepository.hideWarningTrackers = true
+ MainFeatures.FAKE_LOCATION -> localStateRepository.hideWarningLocation = true
+ MainFeatures.IP_SCRAMBLING -> localStateRepository.hideWarningIpScrambling = true
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt
new file mode 100644
index 0000000..882d53f
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 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.domain.usecases
+
+import foundation.e.advancedprivacy.data.repositories.AppListsRepository
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+import foundation.e.advancedprivacy.data.repositories.TrackersRepository
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import foundation.e.privacymodules.trackers.api.IBlockTrackersPrivacyModule
+import foundation.e.privacymodules.trackers.api.ITrackTrackersPrivacyModule
+import foundation.e.privacymodules.trackers.api.Tracker
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class TrackersStateUseCase(
+ private val blockTrackersPrivacyModule: IBlockTrackersPrivacyModule,
+ private val trackersPrivacyModule: ITrackTrackersPrivacyModule,
+ private val localStateRepository: LocalStateRepository,
+ private val trackersRepository: TrackersRepository,
+ private val appListsRepository: AppListsRepository,
+ private val coroutineScope: CoroutineScope
+) {
+ init {
+ trackersPrivacyModule.start(
+ trackers = trackersRepository.trackers,
+ getAppByAPId = appListsRepository::getApp,
+ getAppByUid = appListsRepository::getApp,
+ enableNotification = false
+ )
+ coroutineScope.launch {
+ localStateRepository.blockTrackers.collect { enabled ->
+ if (enabled) {
+ blockTrackersPrivacyModule.enableBlocking()
+ } else {
+ blockTrackersPrivacyModule.disableBlocking()
+ }
+ updateAllTrackersBlockedState()
+ }
+ }
+ }
+
+ private fun updateAllTrackersBlockedState() {
+ localStateRepository.areAllTrackersBlocked.value = blockTrackersPrivacyModule.isBlockingEnabled() &&
+ blockTrackersPrivacyModule.isWhiteListEmpty()
+ }
+
+ fun isWhitelisted(app: ApplicationDescription): Boolean {
+ return isWhitelisted(app, appListsRepository, blockTrackersPrivacyModule)
+ }
+
+ fun toggleAppWhitelist(app: ApplicationDescription, isWhitelisted: Boolean) {
+ appListsRepository.applyForHiddenApps(app) {
+ blockTrackersPrivacyModule.setWhiteListed(it, isWhitelisted)
+ }
+ updateAllTrackersBlockedState()
+ }
+
+ fun blockTracker(app: ApplicationDescription, tracker: Tracker, isBlocked: Boolean) {
+ appListsRepository.applyForHiddenApps(app) {
+ blockTrackersPrivacyModule.setWhiteListed(tracker, it, !isBlocked)
+ }
+ updateAllTrackersBlockedState()
+ }
+
+ fun clearWhitelist(app: ApplicationDescription) {
+ appListsRepository.applyForHiddenApps(
+ app,
+ blockTrackersPrivacyModule::clearWhiteList
+ )
+ updateAllTrackersBlockedState()
+ }
+
+ fun updateTrackers() = coroutineScope.launch {
+ trackersRepository.update()
+ trackersPrivacyModule.start(
+ trackers = trackersRepository.trackers,
+ getAppByAPId = appListsRepository::getApp,
+ getAppByUid = appListsRepository::getApp,
+ enableNotification = false
+ )
+ }
+}
+
+fun isWhitelisted(
+ app: ApplicationDescription,
+ appListsRepository: AppListsRepository,
+ blockTrackersPrivacyModule: IBlockTrackersPrivacyModule
+): Boolean {
+ return appListsRepository.anyForHiddenApps(app, blockTrackersPrivacyModule::isWhitelisted)
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt
new file mode 100644
index 0000000..43e4496
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2021 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.domain.usecases
+
+import android.content.res.Resources
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.throttleFirst
+import foundation.e.advancedprivacy.data.repositories.AppListsRepository
+import foundation.e.advancedprivacy.domain.entities.AppWithCounts
+import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import foundation.e.privacymodules.trackers.api.IBlockTrackersPrivacyModule
+import foundation.e.privacymodules.trackers.api.ITrackTrackersPrivacyModule
+import foundation.e.privacymodules.trackers.api.Tracker
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+
+class TrackersStatisticsUseCase(
+ private val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule,
+ private val blockTrackersPrivacyModule: IBlockTrackersPrivacyModule,
+ private val appListsRepository: AppListsRepository,
+ private val resources: Resources
+) {
+ fun initAppList() {
+ appListsRepository.apps()
+ }
+
+ private fun rawUpdates(): Flow<Unit> = callbackFlow {
+ val listener = object : ITrackTrackersPrivacyModule.Listener {
+ override fun onNewData() {
+ trySend(Unit)
+ }
+ }
+ trackTrackersPrivacyModule.addListener(listener)
+ awaitClose { trackTrackersPrivacyModule.removeListener(listener) }
+ }
+
+ @OptIn(FlowPreview::class)
+ fun listenUpdates(debounce: Duration = 1.seconds) = rawUpdates()
+ .throttleFirst(windowDuration = debounce)
+ .onStart { emit(Unit) }
+
+ fun getDayStatistics(): Pair<TrackersPeriodicStatistics, Int> {
+ return TrackersPeriodicStatistics(
+ callsBlockedNLeaked = trackTrackersPrivacyModule.getPastDayTrackersCalls(),
+ periods = buildDayLabels(),
+ trackersCount = trackTrackersPrivacyModule.getPastDayTrackersCount(),
+ graduations = buildDayGraduations(),
+ ) to trackTrackersPrivacyModule.getTrackersCount()
+ }
+
+ fun getNonBlockedTrackersCount(): Flow<Int> {
+ return if (blockTrackersPrivacyModule.isBlockingEnabled())
+ appListsRepository.allApps().map { apps ->
+ val whiteListedTrackers = mutableSetOf<Tracker>()
+ val whiteListedApps = blockTrackersPrivacyModule.getWhiteListedApp()
+ apps.forEach { app ->
+ if (app in whiteListedApps) {
+ whiteListedTrackers.addAll(trackTrackersPrivacyModule.getTrackersForApp(app))
+ } else {
+ whiteListedTrackers.addAll(blockTrackersPrivacyModule.getWhiteList(app))
+ }
+ }
+ whiteListedTrackers.size
+ }
+ else flowOf(trackTrackersPrivacyModule.getTrackersCount())
+ }
+
+ fun getMostLeakedApp(): ApplicationDescription? {
+ return trackTrackersPrivacyModule.getPastDayMostLeakedApp()
+ }
+
+ fun getDayTrackersCalls() = trackTrackersPrivacyModule.getPastDayTrackersCalls()
+
+ fun getDayTrackersCount() = trackTrackersPrivacyModule.getPastDayTrackersCount()
+
+ private fun buildDayGraduations(): List<String?> {
+ val formatter = DateTimeFormatter.ofPattern(
+ resources.getString(R.string.trackers_graph_hours_period_format)
+ )
+
+ val periods = mutableListOf<String?>()
+ var end = ZonedDateTime.now()
+ for (i in 1..24) {
+ val start = end.truncatedTo(ChronoUnit.HOURS)
+ periods.add(if (start.hour % 6 == 0) formatter.format(start) else null)
+ end = start.minus(1, ChronoUnit.MINUTES)
+ }
+ return periods.reversed()
+ }
+
+ private fun buildDayLabels(): List<String> {
+ val formatter = DateTimeFormatter.ofPattern(
+ resources.getString(R.string.trackers_graph_hours_period_format)
+ )
+ val periods = mutableListOf<String>()
+ var end = ZonedDateTime.now()
+ for (i in 1..24) {
+ val start = end.truncatedTo(ChronoUnit.HOURS)
+ periods.add("${formatter.format(start)} - ${formatter.format(end)}")
+ end = start.minus(1, ChronoUnit.MINUTES)
+ }
+ return periods.reversed()
+ }
+
+ private fun buildMonthLabels(): List<String> {
+ val formater = DateTimeFormatter.ofPattern(
+ resources.getString(R.string.trackers_graph_days_period_format)
+ )
+ val periods = mutableListOf<String>()
+ var day = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS)
+ for (i in 1..30) {
+ periods.add(formater.format(day))
+ day = day.minus(1, ChronoUnit.DAYS)
+ }
+ return periods.reversed()
+ }
+
+ private fun buildYearLabels(): List<String> {
+ val formater = DateTimeFormatter.ofPattern(
+ resources.getString(R.string.trackers_graph_months_period_format)
+ )
+ val periods = mutableListOf<String>()
+ var month = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1)
+ for (i in 1..12) {
+ periods.add(formater.format(month))
+ month = month.minus(1, ChronoUnit.MONTHS)
+ }
+ return periods.reversed()
+ }
+
+ fun getDayMonthYearStatistics(): Triple<TrackersPeriodicStatistics, TrackersPeriodicStatistics, TrackersPeriodicStatistics> {
+ return with(trackTrackersPrivacyModule) {
+ Triple(
+ TrackersPeriodicStatistics(
+ callsBlockedNLeaked = getPastDayTrackersCalls(),
+ periods = buildDayLabels(),
+ trackersCount = getPastDayTrackersCount()
+ ),
+ TrackersPeriodicStatistics(
+ callsBlockedNLeaked = getPastMonthTrackersCalls(),
+ periods = buildMonthLabels(),
+ trackersCount = getPastMonthTrackersCount()
+ ),
+ TrackersPeriodicStatistics(
+ callsBlockedNLeaked = getPastYearTrackersCalls(),
+ periods = buildYearLabels(),
+ trackersCount = getPastYearTrackersCount()
+ )
+ )
+ }
+ }
+
+ fun getTrackersWithWhiteList(app: ApplicationDescription): List<Pair<Tracker, Boolean>> {
+ return appListsRepository.mapReduceForHiddenApps(
+ app = app,
+ map = { appDesc: ApplicationDescription ->
+ (
+ trackTrackersPrivacyModule.getTrackersForApp(appDesc) to
+ blockTrackersPrivacyModule.getWhiteList(appDesc)
+ )
+ },
+ reduce = { lists ->
+ lists.unzip().let { (trackerLists, whiteListedIdLists) ->
+ val whiteListedIds = whiteListedIdLists.flatten().map { it.id }.toSet()
+
+ trackerLists.flatten().distinctBy { it.id }.sortedBy { it.label.lowercase() }
+ .map { tracker -> tracker to (tracker.id in whiteListedIds) }
+ }
+ }
+ )
+ }
+
+ fun isWhiteListEmpty(app: ApplicationDescription): Boolean {
+ return appListsRepository.mapReduceForHiddenApps(
+ app = app,
+ map = { appDesc: ApplicationDescription ->
+ blockTrackersPrivacyModule.getWhiteList(appDesc).isEmpty()
+ },
+ reduce = { areEmpty -> areEmpty.all { it } }
+ )
+ }
+
+ fun getCalls(app: ApplicationDescription): Pair<Int, Int> {
+ return appListsRepository.mapReduceForHiddenApps(
+ app = app,
+ map = trackTrackersPrivacyModule::getPastDayTrackersCallsForApp,
+ reduce = { zip ->
+ zip.unzip().let { (blocked, leaked) ->
+ blocked.sum() to leaked.sum()
+ }
+ }
+ )
+ }
+
+ fun getAppsWithCounts(): Flow<List<AppWithCounts>> {
+ val trackersCounts = trackTrackersPrivacyModule.getTrackersCountByApp()
+ val hiddenAppsTrackersWithWhiteList =
+ getTrackersWithWhiteList(appListsRepository.dummySystemApp)
+ val acAppsTrackersWithWhiteList =
+ getTrackersWithWhiteList(appListsRepository.dummyCompatibilityApp)
+
+ return appListsRepository.apps()
+ .map { apps ->
+ val callsByApp = trackTrackersPrivacyModule.getPastDayTrackersCallsByApps()
+ apps.map { app ->
+ val calls = appListsRepository.mapReduceForHiddenApps(
+ app = app,
+ map = { callsByApp.getOrDefault(app, 0 to 0) },
+ reduce = {
+ it.unzip().let { (blocked, leaked) ->
+ blocked.sum() to leaked.sum()
+ }
+ }
+ )
+
+ AppWithCounts(
+ app = app,
+ isWhitelisted = !blockTrackersPrivacyModule.isBlockingEnabled() ||
+ isWhitelisted(app, appListsRepository, blockTrackersPrivacyModule),
+ trackersCount = when (app) {
+ appListsRepository.dummySystemApp ->
+ hiddenAppsTrackersWithWhiteList.size
+ appListsRepository.dummyCompatibilityApp ->
+ acAppsTrackersWithWhiteList.size
+ else -> trackersCounts.getOrDefault(app, 0)
+ },
+ whiteListedTrackersCount = when (app) {
+ appListsRepository.dummySystemApp ->
+ hiddenAppsTrackersWithWhiteList.count { it.second }
+ appListsRepository.dummyCompatibilityApp ->
+ acAppsTrackersWithWhiteList.count { it.second }
+ else ->
+ blockTrackersPrivacyModule.getWhiteList(app).size
+ },
+ blockedLeaks = calls.first,
+ leaks = calls.second
+ )
+ }
+ .sortedWith(mostLeakedAppsComparator)
+ }
+ }
+
+ private val mostLeakedAppsComparator: Comparator<AppWithCounts> = Comparator { o1, o2 ->
+ val leaks = o2.leaks - o1.leaks
+ if (leaks != 0) leaks else {
+ val whitelisted = o2.whiteListedTrackersCount - o1.whiteListedTrackersCount
+ if (whitelisted != 0) whitelisted else {
+ o2.trackersCount - o1.trackersCount
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/UpdateWidgetUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/UpdateWidgetUseCase.kt
new file mode 100644
index 0000000..94c734c
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/UpdateWidgetUseCase.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.usecases
+
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+import foundation.e.privacymodules.trackers.api.ITrackTrackersPrivacyModule
+
+class UpdateWidgetUseCase(
+ private val localStateRepository: LocalStateRepository,
+ private val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule,
+) {
+ init {
+ trackTrackersPrivacyModule.addListener(object : ITrackTrackersPrivacyModule.Listener {
+ override fun onNewData() {
+ }
+ })
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt
new file mode 100644
index 0000000..b30935c
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt
@@ -0,0 +1,307 @@
+/*
+ * 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.features.dashboard
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.Html
+import android.text.Html.FROM_HTML_MODE_LEGACY
+import android.view.View
+import android.widget.Toast
+import androidx.core.content.ContextCompat.getColor
+import androidx.core.os.bundleOf
+import androidx.core.view.isVisible
+import androidx.fragment.app.commit
+import androidx.fragment.app.replace
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import foundation.e.advancedprivacy.AdvancedPrivacyApplication
+import foundation.e.advancedprivacy.DependencyContainer
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.GraphHolder
+import foundation.e.advancedprivacy.common.NavToolbarFragment
+import foundation.e.advancedprivacy.databinding.FragmentDashboardBinding
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import foundation.e.advancedprivacy.domain.entities.LocationMode
+import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState
+import foundation.e.advancedprivacy.domain.entities.TrackerMode
+import foundation.e.advancedprivacy.features.dashboard.DashboardViewModel.Action
+import foundation.e.advancedprivacy.features.dashboard.DashboardViewModel.SingleEvent
+import foundation.e.advancedprivacy.features.internetprivacy.InternetPrivacyFragment
+import foundation.e.advancedprivacy.features.location.FakeLocationFragment
+import foundation.e.advancedprivacy.features.trackers.TrackersFragment
+import foundation.e.advancedprivacy.features.trackers.apptrackers.AppTrackersFragment
+import kotlinx.coroutines.launch
+
+class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) {
+ companion object {
+ private const val PARAM_HIGHLIGHT_INDEX = "PARAM_HIGHLIGHT_INDEX"
+ fun buildArgs(highlightIndex: Int): Bundle = bundleOf(
+ PARAM_HIGHLIGHT_INDEX to highlightIndex
+ )
+ }
+
+ private val dependencyContainer: DependencyContainer by lazy {
+ (this.requireActivity().application as AdvancedPrivacyApplication).dependencyContainer
+ }
+
+ private val viewModel: DashboardViewModel by viewModels {
+ dependencyContainer.viewModelsFactory
+ }
+
+ private var graphHolder: GraphHolder? = null
+
+ private var _binding: FragmentDashboardBinding? = null
+ private val binding get() = _binding!!
+
+ private var highlightIndexOnStart: Int? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ highlightIndexOnStart = arguments?.getInt(PARAM_HIGHLIGHT_INDEX, -1)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentDashboardBinding.bind(view)
+
+ graphHolder = GraphHolder(binding.graph, requireContext())
+
+ binding.leakingAppButton.setOnClickListener {
+ viewModel.submitAction(Action.ShowMostLeakedApp)
+ }
+ binding.toggleTrackers.setOnClickListener {
+ viewModel.submitAction(Action.ToggleTrackers)
+ }
+ binding.toggleLocation.setOnClickListener {
+ viewModel.submitAction(Action.ToggleLocation)
+ }
+ binding.toggleIpscrambling.setOnClickListener {
+ viewModel.submitAction(Action.ToggleIpScrambling)
+ }
+ binding.myLocation.container.setOnClickListener {
+ viewModel.submitAction(Action.ShowFakeMyLocationAction)
+ }
+ binding.internetActivityPrivacy.container.setOnClickListener {
+ viewModel.submitAction(Action.ShowInternetActivityPrivacyAction)
+ }
+ binding.appsPermissions.container.setOnClickListener {
+ viewModel.submitAction(Action.ShowAppsPermissions)
+ }
+
+ binding.amITracked.container.setOnClickListener {
+ viewModel.submitAction(Action.ShowTrackers)
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ when (event) {
+ is SingleEvent.NavigateToLocationSingleEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<FakeLocationFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ is SingleEvent.NavigateToInternetActivityPrivacySingleEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<InternetPrivacyFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ is SingleEvent.NavigateToPermissionsSingleEvent -> {
+ val intent = Intent("android.intent.action.MANAGE_PERMISSIONS")
+ requireActivity().startActivity(intent)
+ }
+ SingleEvent.NavigateToTrackersSingleEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<TrackersFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ is SingleEvent.NavigateToAppDetailsEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<AppTrackersFragment>(
+ R.id.container,
+ args = AppTrackersFragment.buildArgs(
+ event.appDesc.label.toString(),
+ event.appDesc.packageName,
+ event.appDesc.uid
+ )
+ )
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ is SingleEvent.ToastMessageSingleEvent ->
+ Toast.makeText(
+ requireContext(),
+ getString(event.message, *event.args.toTypedArray()),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
+ }
+
+ override fun getTitle(): String {
+ return getString(R.string.dashboard_title)
+ }
+
+ private fun render(state: DashboardState) {
+ binding.stateLabel.text = getString(
+ when (state.quickPrivacyState) {
+ QuickPrivacyState.DISABLED -> R.string.dashboard_state_title_off
+ QuickPrivacyState.FULL_ENABLED -> R.string.dashboard_state_title_on
+ QuickPrivacyState.ENABLED -> R.string.dashboard_state_title_custom
+ }
+ )
+
+ binding.stateIcon.setImageResource(
+ if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on
+ else R.drawable.ic_shield_off
+ )
+
+ binding.toggleTrackers.isChecked = state.trackerMode != TrackerMode.VULNERABLE
+
+ binding.stateTrackers.text = getString(
+ when (state.trackerMode) {
+ TrackerMode.DENIED -> R.string.dashboard_state_trackers_on
+ TrackerMode.VULNERABLE -> R.string.dashboard_state_trackers_off
+ TrackerMode.CUSTOM -> R.string.dashboard_state_trackers_custom
+ }
+ )
+ binding.stateTrackers.setTextColor(
+ getColor(
+ requireContext(),
+ if (state.trackerMode == TrackerMode.VULNERABLE) R.color.red_off
+ else R.color.green_valid
+ )
+ )
+
+ binding.toggleLocation.isChecked = state.isLocationHidden
+
+ binding.stateGeolocation.text = getString(
+ if (state.isLocationHidden) R.string.dashboard_state_geolocation_on
+ else R.string.dashboard_state_geolocation_off
+ )
+ binding.stateGeolocation.setTextColor(
+ getColor(
+ requireContext(),
+ if (state.isLocationHidden) R.color.green_valid
+ else R.color.red_off
+ )
+ )
+
+ binding.toggleIpscrambling.isChecked = state.ipScramblingMode.isChecked
+ val isLoading = state.ipScramblingMode.isLoading
+
+ binding.stateIpAddress.text = getString(
+ if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.dashboard_state_ipaddress_on
+ else R.string.dashboard_state_ipaddress_off
+ )
+
+ binding.stateIpAddressLoader.visibility = if (isLoading) View.VISIBLE else View.GONE
+ binding.stateIpAddress.visibility = if (!isLoading) View.VISIBLE else View.GONE
+
+ binding.stateIpAddress.setTextColor(
+ getColor(
+ requireContext(),
+ if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.color.green_valid
+ else R.color.red_off
+ )
+ )
+
+ if (state.dayStatistics?.all { it.first == 0 && it.second == 0 } == true) {
+ binding.graph.visibility = View.INVISIBLE
+ binding.graphLegend.isVisible = false
+ binding.leakingAppButton.isVisible = false
+ binding.graphEmpty.isVisible = true
+ } else {
+ binding.graph.isVisible = true
+ binding.graphLegend.isVisible = true
+ binding.leakingAppButton.isVisible = true
+ binding.graphEmpty.isVisible = false
+ state.dayStatistics?.let { graphHolder?.data = it }
+ state.dayLabels?.let { graphHolder?.labels = it }
+ state.dayGraduations?.let { graphHolder?.graduations = it }
+
+ binding.graphLegend.text = Html.fromHtml(
+ getString(
+ R.string.dashboard_graph_trackers_legend,
+ state.leakedTrackersCount?.toString() ?: "No"
+ ),
+ FROM_HTML_MODE_LEGACY
+ )
+
+ highlightIndexOnStart?.let {
+ binding.graph.post {
+ graphHolder?.highlightIndex(it)
+ }
+ highlightIndexOnStart = null
+ }
+ }
+
+ if (state.allowedTrackersCount != null && state.trackersCount != null) {
+ binding.amITracked.subTitle = getString(R.string.dashboard_am_i_tracked_subtitle, state.trackersCount, state.allowedTrackersCount)
+ } else {
+ binding.amITracked.subTitle = ""
+ }
+
+ binding.myLocation.subTitle = getString(
+ when (state.locationMode) {
+ LocationMode.REAL_LOCATION -> R.string.dashboard_location_subtitle_off
+ LocationMode.SPECIFIC_LOCATION -> R.string.dashboard_location_subtitle_specific
+ LocationMode.RANDOM_LOCATION -> R.string.dashboard_location_subtitle_random
+ }
+ )
+
+ binding.internetActivityPrivacy.subTitle = getString(
+ if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.dashboard_internet_activity_privacy_subtitle_on
+ else R.string.dashboard_internet_activity_privacy_subtitle_off
+ )
+
+ binding.executePendingBindings()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ graphHolder = null
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt
new file mode 100644
index 0000000..8fc8767
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.features.dashboard
+
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import foundation.e.advancedprivacy.domain.entities.LocationMode
+import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState
+import foundation.e.advancedprivacy.domain.entities.TrackerMode
+
+data class DashboardState(
+ val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED,
+ val trackerMode: TrackerMode = TrackerMode.VULNERABLE,
+ val isLocationHidden: Boolean = false,
+ val ipScramblingMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP_LOADING,
+ val locationMode: LocationMode = LocationMode.REAL_LOCATION,
+ val leakedTrackersCount: Int? = null,
+ val trackersCount: Int? = null,
+ val allowedTrackersCount: Int? = null,
+ val dayStatistics: List<Pair<Int, Int>>? = null,
+ val dayLabels: List<String>? = null,
+ val dayGraduations: List<String?>? = null,
+)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt
new file mode 100644
index 0000000..d82b073
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt
@@ -0,0 +1,158 @@
+/*
+* Copyright (C) 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.features.dashboard
+
+import androidx.annotation.StringRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class DashboardViewModel(
+ private val getPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
+ private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
+) : ViewModel() {
+
+ private val _state = MutableStateFlow(DashboardState())
+ val state = _state.asStateFlow()
+
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) { trackersStatisticsUseCase.initAppList() }
+ }
+
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ merge(
+ getPrivacyStateUseCase.quickPrivacyState.map {
+ _state.update { s -> s.copy(quickPrivacyState = it) }
+ },
+ getPrivacyStateUseCase.ipScramblingMode.map {
+ _state.update { s -> s.copy(ipScramblingMode = it) }
+ },
+ trackersStatisticsUseCase.listenUpdates().flatMapLatest {
+ fetchStatistics()
+ },
+ getPrivacyStateUseCase.trackerMode.map {
+ _state.update { s -> s.copy(trackerMode = it) }
+ },
+ getPrivacyStateUseCase.isLocationHidden.map {
+ _state.update { s -> s.copy(isLocationHidden = it) }
+ },
+ getPrivacyStateUseCase.locationMode.map {
+ _state.update { s -> s.copy(locationMode = it) }
+ },
+ getPrivacyStateUseCase.otherVpnRunning.map {
+ _singleEvents.emit(
+ SingleEvent.ToastMessageSingleEvent(
+ R.string.ipscrambling_error_always_on_vpn_already_running,
+ listOf(it.label ?: "")
+ )
+ )
+ }
+ ).collect {}
+ }
+
+ fun submitAction(action: Action) = viewModelScope.launch {
+ when (action) {
+ is Action.ToggleTrackers -> {
+ getPrivacyStateUseCase.toggleTrackers()
+ // Add delay here to prevent race condition with trackers state.
+ delay(200)
+ fetchStatistics().first()
+ }
+ is Action.ToggleLocation -> getPrivacyStateUseCase.toggleLocation()
+ is Action.ToggleIpScrambling -> getPrivacyStateUseCase.toggleIpScrambling()
+ is Action.ShowFakeMyLocationAction ->
+ _singleEvents.emit(SingleEvent.NavigateToLocationSingleEvent)
+ is Action.ShowAppsPermissions ->
+ _singleEvents.emit(SingleEvent.NavigateToPermissionsSingleEvent)
+ is Action.ShowInternetActivityPrivacyAction ->
+ _singleEvents.emit(SingleEvent.NavigateToInternetActivityPrivacySingleEvent)
+ is Action.ShowTrackers ->
+ _singleEvents.emit(SingleEvent.NavigateToTrackersSingleEvent)
+ is Action.ShowMostLeakedApp -> actionShowMostLeakedApp()
+ }
+ }
+
+ private suspend fun fetchStatistics(): Flow<Unit> = withContext(Dispatchers.IO) {
+ trackersStatisticsUseCase.getNonBlockedTrackersCount().map { nonBlockedTrackersCount ->
+ trackersStatisticsUseCase.getDayStatistics().let { (dayStatistics, trackersCount) ->
+ _state.update { s ->
+ s.copy(
+ dayStatistics = dayStatistics.callsBlockedNLeaked,
+ dayLabels = dayStatistics.periods,
+ dayGraduations = dayStatistics.graduations,
+ leakedTrackersCount = dayStatistics.trackersCount,
+ trackersCount = trackersCount,
+ allowedTrackersCount = nonBlockedTrackersCount
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun actionShowMostLeakedApp() = withContext(Dispatchers.IO) {
+ _singleEvents.emit(
+ trackersStatisticsUseCase.getMostLeakedApp()?.let {
+ SingleEvent.NavigateToAppDetailsEvent(appDesc = it)
+ } ?: SingleEvent.NavigateToTrackersSingleEvent
+ )
+ }
+
+ sealed class SingleEvent {
+ object NavigateToTrackersSingleEvent : SingleEvent()
+ object NavigateToInternetActivityPrivacySingleEvent : SingleEvent()
+ object NavigateToLocationSingleEvent : SingleEvent()
+ object NavigateToPermissionsSingleEvent : SingleEvent()
+ data class NavigateToAppDetailsEvent(val appDesc: ApplicationDescription) : SingleEvent()
+ data class ToastMessageSingleEvent(
+ @StringRes val message: Int,
+ val args: List<Any> = emptyList()
+ ) : SingleEvent()
+ }
+
+ sealed class Action {
+ object ToggleTrackers : Action()
+ object ToggleLocation : Action()
+ object ToggleIpScrambling : Action()
+ object ShowFakeMyLocationAction : Action()
+ object ShowInternetActivityPrivacyAction : Action()
+ object ShowAppsPermissions : Action()
+ object ShowTrackers : Action()
+ object ShowMostLeakedApp : Action()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt
new file mode 100644
index 0000000..07da82a
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt
@@ -0,0 +1,201 @@
+/*
+ * 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.features.internetprivacy
+
+import android.os.Bundle
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Toast
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import foundation.e.advancedprivacy.AdvancedPrivacyApplication
+import foundation.e.advancedprivacy.DependencyContainer
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.NavToolbarFragment
+import foundation.e.advancedprivacy.common.ToggleAppsAdapter
+import foundation.e.advancedprivacy.common.setToolTipForAsterisk
+import foundation.e.advancedprivacy.databinding.FragmentInternetActivityPolicyBinding
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import kotlinx.coroutines.launch
+import java.util.Locale
+
+class InternetPrivacyFragment : NavToolbarFragment(R.layout.fragment_internet_activity_policy) {
+
+ private val dependencyContainer: DependencyContainer by lazy {
+ (this.requireActivity().application as AdvancedPrivacyApplication).dependencyContainer
+ }
+
+ private val viewModel: InternetPrivacyViewModel by viewModels {
+ dependencyContainer.viewModelsFactory
+ }
+
+ private var _binding: FragmentInternetActivityPolicyBinding? = null
+ private val binding get() = _binding!!
+
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentInternetActivityPolicyBinding.bind(view)
+
+ binding.apps.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ setHasFixedSize(true)
+ adapter = ToggleAppsAdapter(R.layout.ipscrambling_item_app_toggle) { packageName ->
+ viewModel.submitAction(
+ InternetPrivacyViewModel.Action.ToggleAppIpScrambled(packageName)
+ )
+ }
+ }
+
+ binding.radioUseRealIp.radiobutton.setOnClickListener {
+ viewModel.submitAction(InternetPrivacyViewModel.Action.UseRealIPAction)
+ }
+
+ binding.radioUseHiddenIp.radiobutton.setOnClickListener {
+ viewModel.submitAction(InternetPrivacyViewModel.Action.UseHiddenIPAction)
+ }
+
+ setToolTipForAsterisk(
+ textView = binding.ipscramblingSelectApps,
+ textId = R.string.ipscrambling_select_app,
+ tooltipTextId = R.string.ipscrambling_app_list_infos
+ )
+
+ binding.ipscramblingSelectLocation.apply {
+ adapter = ArrayAdapter(
+ requireContext(), android.R.layout.simple_spinner_item,
+ viewModel.availablesLocationsIds.map {
+ if (it == "") {
+ getString(R.string.ipscrambling_any_location)
+ } else {
+ Locale("", it).displayCountry
+ }
+ }
+ ).apply {
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+ }
+
+ onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(
+ parentView: AdapterView<*>,
+ selectedItemView: View?,
+ position: Int,
+ id: Long
+ ) {
+ viewModel.submitAction(
+ InternetPrivacyViewModel.Action.SelectLocationAction(
+ position
+ )
+ )
+ }
+
+ override fun onNothingSelected(parentView: AdapterView<*>?) {}
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ when (event) {
+ is InternetPrivacyViewModel.SingleEvent.ErrorEvent -> {
+ displayToast(getString(event.errorResId, *event.args.toTypedArray()))
+ }
+ }
+ }
+ }
+ }
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
+ }
+
+ override fun getTitle(): String = getString(R.string.ipscrambling_title)
+
+ private fun render(state: InternetPrivacyState) {
+ binding.radioUseHiddenIp.radiobutton.apply {
+ isChecked = state.mode in listOf(
+ InternetPrivacyMode.HIDE_IP,
+ InternetPrivacyMode.HIDE_IP_LOADING
+ )
+ isEnabled = state.mode != InternetPrivacyMode.HIDE_IP_LOADING
+ }
+ binding.radioUseRealIp.radiobutton.apply {
+ isChecked =
+ state.mode in listOf(
+ InternetPrivacyMode.REAL_IP,
+ InternetPrivacyMode.REAL_IP_LOADING
+ )
+ isEnabled = state.mode != InternetPrivacyMode.REAL_IP_LOADING
+ }
+
+ binding.ipscramblingSelectLocation.setSelection(state.selectedLocationPosition)
+
+ // TODO: this should not be mandatory.
+ binding.apps.post {
+ (binding.apps.adapter as ToggleAppsAdapter?)?.setData(
+ list = state.getApps(),
+ isEnabled = state.mode == InternetPrivacyMode.HIDE_IP
+ )
+ }
+
+ val viewIdsToHide = listOf(
+ binding.ipscramblingLocationLabel,
+ binding.selectLocationContainer,
+ binding.ipscramblingSelectLocation,
+ binding.ipscramblingSelectApps,
+ binding.apps
+ )
+
+ when {
+ state.mode in listOf(
+ InternetPrivacyMode.HIDE_IP_LOADING,
+ InternetPrivacyMode.REAL_IP_LOADING
+ )
+ || state.availableApps.isEmpty() -> {
+ binding.loader.visibility = View.VISIBLE
+ viewIdsToHide.forEach { it.visibility = View.GONE }
+ }
+ else -> {
+ binding.loader.visibility = View.GONE
+ viewIdsToHide.forEach { it.visibility = View.VISIBLE }
+ }
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt
new file mode 100644
index 0000000..e0df73b
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.features.internetprivacy
+
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+
+data class InternetPrivacyState(
+ val mode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP,
+ val availableApps: List<ApplicationDescription> = emptyList(),
+ val bypassTorApps: Collection<String> = emptyList(),
+ val selectedLocation: String = "",
+ val availableLocationIds: List<String> = emptyList(),
+ val forceRedraw: Boolean = false,
+) {
+ fun getApps(): List<Pair<ApplicationDescription, Boolean>> {
+ return availableApps.map { it to (it.packageName !in bypassTorApps) }
+ }
+
+ val selectedLocationPosition get() = availableLocationIds.indexOf(selectedLocation)
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt
new file mode 100644
index 0000000..051c8e8
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.features.internetprivacy
+
+import androidx.annotation.StringRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import foundation.e.advancedprivacy.domain.usecases.AppListUseCase
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.IpScramblingStateUseCase
+import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class InternetPrivacyViewModel(
+ private val ipScramblerModule: IIpScramblerModule,
+ private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
+ private val ipScramblingStateUseCase: IpScramblingStateUseCase,
+ private val appListUseCase: AppListUseCase
+) : ViewModel() {
+ companion object {
+ private const val WARNING_LOADING_LONG_DELAY = 5 * 1000L
+ }
+
+ private val _state = MutableStateFlow(InternetPrivacyState())
+ val state = _state.asStateFlow()
+
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
+ val availablesLocationsIds = listOf("", *ipScramblerModule.getAvailablesLocations().sorted().toTypedArray())
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update {
+ it.copy(
+ mode = ipScramblingStateUseCase.internetPrivacyMode.value,
+ availableLocationIds = availablesLocationsIds,
+ selectedLocation = ipScramblerModule.exitCountry
+ )
+ }
+ }
+ }
+
+ @OptIn(FlowPreview::class)
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ launch {
+ merge(
+ appListUseCase.getAppsUsingInternet().map { apps ->
+ _state.update { s ->
+ s.copy(
+ availableApps = apps,
+ bypassTorApps = ipScramblingStateUseCase.bypassTorApps
+ )
+ }
+ },
+ ipScramblingStateUseCase.internetPrivacyMode.map {
+ _state.update { s -> s.copy(mode = it) }
+ }
+ ).collect {}
+ }
+
+ launch {
+ ipScramblingStateUseCase.internetPrivacyMode
+ .map { it == InternetPrivacyMode.HIDE_IP_LOADING }
+ .debounce(WARNING_LOADING_LONG_DELAY)
+ .collect {
+ if (it) _singleEvents.emit(
+ SingleEvent.ErrorEvent(R.string.ipscrambling_warning_starting_long)
+ )
+ }
+ }
+
+ launch {
+ getQuickPrivacyStateUseCase.otherVpnRunning.collect {
+ _singleEvents.emit(
+ SingleEvent.ErrorEvent(
+ R.string.ipscrambling_error_always_on_vpn_already_running,
+ listOf(it.label ?: "")
+ )
+ )
+ _state.update { it.copy(forceRedraw = !it.forceRedraw) }
+ }
+ }
+ }
+
+ fun submitAction(action: Action) = viewModelScope.launch {
+ when (action) {
+ is Action.UseRealIPAction -> actionUseRealIP()
+ is Action.UseHiddenIPAction -> actionUseHiddenIP()
+ is Action.ToggleAppIpScrambled -> actionToggleAppIpScrambled(action)
+ is Action.SelectLocationAction -> actionSelectLocation(action)
+ }
+ }
+
+ private fun actionUseRealIP() {
+ ipScramblingStateUseCase.toggle(hideIp = false)
+ }
+
+ private fun actionUseHiddenIP() {
+ ipScramblingStateUseCase.toggle(hideIp = true)
+ }
+
+ private suspend fun actionToggleAppIpScrambled(action: Action.ToggleAppIpScrambled) = withContext(Dispatchers.IO) {
+ ipScramblingStateUseCase.toggleBypassTor(action.packageName)
+ _state.update { it.copy(bypassTorApps = ipScramblingStateUseCase.bypassTorApps) }
+ }
+
+ private suspend fun actionSelectLocation(action: Action.SelectLocationAction) = withContext(Dispatchers.IO) {
+ val locationId = _state.value.availableLocationIds[action.position]
+ if (locationId != ipScramblerModule.exitCountry) {
+ ipScramblerModule.exitCountry = locationId
+ _state.update { it.copy(selectedLocation = locationId) }
+ }
+ }
+
+ sealed class SingleEvent {
+ data class ErrorEvent(
+ @StringRes val errorResId: Int,
+ val args: List<Any> = emptyList()
+ ) : SingleEvent()
+ }
+
+ sealed class Action {
+ object UseRealIPAction : Action()
+ object UseHiddenIPAction : Action()
+ data class ToggleAppIpScrambled(val packageName: String) : Action()
+ data class SelectLocationAction(val position: Int) : Action()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt
new file mode 100644
index 0000000..9934713
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt
@@ -0,0 +1,376 @@
+/*
+ * 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.features.location
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Context
+import android.location.Location
+import android.os.Bundle
+import android.text.Editable
+import android.view.View
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.NonNull
+import androidx.core.view.isVisible
+import androidx.core.widget.addTextChangedListener
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM
+import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
+import com.mapbox.mapboxsdk.Mapbox
+import com.mapbox.mapboxsdk.WellKnownTileServer
+import com.mapbox.mapboxsdk.camera.CameraPosition
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
+import com.mapbox.mapboxsdk.geometry.LatLng
+import com.mapbox.mapboxsdk.location.LocationComponent
+import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions
+import com.mapbox.mapboxsdk.location.modes.CameraMode
+import com.mapbox.mapboxsdk.location.modes.RenderMode
+import com.mapbox.mapboxsdk.maps.MapboxMap
+import com.mapbox.mapboxsdk.maps.Style
+import foundation.e.advancedprivacy.AdvancedPrivacyApplication
+import foundation.e.advancedprivacy.DependencyContainer
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.NavToolbarFragment
+import foundation.e.advancedprivacy.databinding.FragmentFakeLocationBinding
+import foundation.e.advancedprivacy.domain.entities.LocationMode
+import foundation.e.advancedprivacy.features.location.FakeLocationViewModel.Action
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+
+class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) {
+
+ private var isFirstLaunch: Boolean = true
+
+ private val dependencyContainer: DependencyContainer by lazy {
+ (this.requireActivity().application as AdvancedPrivacyApplication).dependencyContainer
+ }
+
+ private val viewModel: FakeLocationViewModel by viewModels {
+ dependencyContainer.viewModelsFactory
+ }
+
+ private var _binding: FragmentFakeLocationBinding? = null
+ private val binding get() = _binding!!
+
+ private var mapboxMap: MapboxMap? = null
+ private var locationComponent: LocationComponent? = null
+
+ private var inputJob: Job? = null
+
+ private val locationPermissionRequest = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ if (permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) ||
+ permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false)
+ ) {
+ viewModel.submitAction(Action.StartListeningLocation)
+ } // TODO: else.
+ }
+
+ companion object {
+ private const val DEBOUNCE_PERIOD = 1000L
+ private const val MAP_STYLE = "mapbox://styles/mapbox/outdoors-v12"
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ Mapbox.getInstance(requireContext(), getString(R.string.mapbox_key), WellKnownTileServer.Mapbox)
+ }
+
+ override fun getTitle(): String = getString(R.string.location_title)
+
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentFakeLocationBinding.bind(view)
+
+ binding.mapView.setup(savedInstanceState) { mapboxMap ->
+ this.mapboxMap = mapboxMap
+ mapboxMap.uiSettings.isRotateGesturesEnabled = false
+ mapboxMap.setStyle(MAP_STYLE) { style ->
+ enableLocationPlugin(style)
+ mapboxMap.addOnCameraMoveListener {
+ if (binding.mapView.isEnabled) {
+ mapboxMap.cameraPosition.target?.let {
+ viewModel.submitAction(
+ Action.SetSpecificLocationAction(
+ it.latitude.toFloat(),
+ it.longitude.toFloat()
+ )
+ )
+ }
+ }
+ }
+
+ mapboxMap.cameraPosition = CameraPosition.Builder().zoom(8.0).build()
+
+ // Bind click listeners once map is ready.
+ bindClickListeners()
+
+ render(viewModel.state.value)
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ if (event is FakeLocationViewModel.SingleEvent.LocationUpdatedEvent) {
+ updateLocation(event.location, event.mode)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ when (event) {
+ is FakeLocationViewModel.SingleEvent.ErrorEvent -> {
+ displayToast(event.error)
+ }
+ is FakeLocationViewModel.SingleEvent.RequestLocationPermission -> {
+ // TODO for standalone: rationale dialog
+ locationPermissionRequest.launch(
+ arrayOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ )
+ )
+ }
+ is FakeLocationViewModel.SingleEvent.LocationUpdatedEvent -> {
+ // Nothing here, another collect linked to mapbox view.
+ }
+ }
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
+ }
+
+ private fun getCoordinatesAfterTextChanged(
+ inputLayout: TextInputLayout,
+ editText: TextInputEditText,
+ isLat: Boolean
+ ) = { editable: Editable? ->
+ inputJob?.cancel()
+ if (editable != null && editable.isNotEmpty() && editText.isEnabled) {
+ inputJob = lifecycleScope.launch {
+ delay(DEBOUNCE_PERIOD)
+ ensureActive()
+ try {
+ val value = editable.toString().toFloat()
+ val maxValue = if (isLat) 90f else 180f
+
+ if (value > maxValue || value < -maxValue) {
+ throw NumberFormatException("value $value is out of bounds")
+ }
+ inputLayout.error = null
+
+ inputLayout.setEndIconDrawable(R.drawable.ic_valid)
+ inputLayout.endIconMode = END_ICON_CUSTOM
+
+ // Here, value is valid, try to send the values
+ try {
+ val lat = binding.edittextLatitude.text.toString().toFloat()
+ val lon = binding.edittextLongitude.text.toString().toFloat()
+ if (lat <= 90f && lat >= -90f && lon <= 180f && lon >= -180f) {
+ mapboxMap?.moveCamera(
+ CameraUpdateFactory.newLatLng(
+ LatLng(lat.toDouble(), lon.toDouble())
+ )
+ )
+ }
+ } catch (e: NumberFormatException) {
+ }
+ } catch (e: NumberFormatException) {
+ inputLayout.endIconMode = END_ICON_NONE
+ inputLayout.error = getString(R.string.location_input_error)
+ }
+ }
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun bindClickListeners() {
+ binding.radioUseRealLocation.setOnClickListener {
+ viewModel.submitAction(Action.UseRealLocationAction)
+ }
+ binding.radioUseRandomLocation.setOnClickListener {
+ viewModel.submitAction(Action.UseRandomLocationAction)
+ }
+ binding.radioUseSpecificLocation.setOnClickListener {
+ mapboxMap?.cameraPosition?.target?.let {
+ viewModel.submitAction(
+ Action.SetSpecificLocationAction(it.latitude.toFloat(), it.longitude.toFloat())
+ )
+ }
+ }
+ binding.edittextLatitude.addTextChangedListener(
+ afterTextChanged = getCoordinatesAfterTextChanged(
+ binding.textlayoutLatitude,
+ binding.edittextLatitude,
+ true
+ )
+ )
+
+ binding.edittextLongitude.addTextChangedListener(
+ afterTextChanged = getCoordinatesAfterTextChanged(
+ binding.textlayoutLongitude,
+ binding.edittextLongitude,
+ false
+ )
+ )
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun render(state: FakeLocationState) {
+ binding.radioUseRandomLocation.isChecked = state.mode == LocationMode.RANDOM_LOCATION
+
+ binding.radioUseSpecificLocation.isChecked = state.mode == LocationMode.SPECIFIC_LOCATION
+
+ binding.radioUseRealLocation.isChecked = state.mode == LocationMode.REAL_LOCATION
+
+ binding.mapView.isEnabled = (state.mode == LocationMode.SPECIFIC_LOCATION)
+
+ if (state.mode == LocationMode.REAL_LOCATION) {
+ binding.centeredMarker.isVisible = false
+ } else {
+ binding.mapLoader.isVisible = false
+ binding.mapOverlay.isVisible = state.mode != LocationMode.SPECIFIC_LOCATION
+ binding.centeredMarker.isVisible = true
+
+ mapboxMap?.moveCamera(
+ CameraUpdateFactory.newLatLng(
+ LatLng(
+ state.specificLatitude?.toDouble() ?: 0.0,
+ state.specificLongitude?.toDouble() ?: 0.0
+ )
+ )
+ )
+ }
+
+ binding.textlayoutLatitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION)
+ binding.textlayoutLongitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION)
+
+ binding.edittextLatitude.setText(state.specificLatitude?.toString())
+ binding.edittextLongitude.setText(state.specificLongitude?.toString())
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun updateLocation(lastLocation: Location?, mode: LocationMode) {
+ lastLocation?.let { location ->
+ locationComponent?.isLocationComponentEnabled = true
+ locationComponent?.forceLocationUpdate(location)
+
+ if (mode == LocationMode.REAL_LOCATION) {
+ binding.mapLoader.isVisible = false
+ binding.mapOverlay.isVisible = false
+
+ val update = CameraUpdateFactory.newLatLng(
+ LatLng(location.latitude, location.longitude)
+ )
+
+ if (isFirstLaunch) {
+ mapboxMap?.moveCamera(update)
+ isFirstLaunch = false
+ } else {
+ mapboxMap?.animateCamera(update)
+ }
+ }
+ } ?: run {
+ locationComponent?.isLocationComponentEnabled = false
+ if (mode == LocationMode.REAL_LOCATION) {
+ binding.mapLoader.isVisible = true
+ binding.mapOverlay.isVisible = true
+ }
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun enableLocationPlugin(@NonNull loadedMapStyle: Style) {
+ // Check if permissions are enabled and if not request
+ locationComponent = mapboxMap?.locationComponent
+ locationComponent?.activateLocationComponent(
+ LocationComponentActivationOptions.builder(
+ requireContext(), loadedMapStyle
+ ).useDefaultLocationEngine(false).build()
+ )
+ locationComponent?.isLocationComponentEnabled = true
+ locationComponent?.cameraMode = CameraMode.NONE
+ locationComponent?.renderMode = RenderMode.NORMAL
+ }
+
+ override fun onStart() {
+ super.onStart()
+ binding.mapView.onStart()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ viewModel.submitAction(Action.StartListeningLocation)
+ binding.mapView.onResume()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ viewModel.submitAction(Action.StopListeningLocation)
+ binding.mapView.onPause()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ binding.mapView.onStop()
+ }
+
+ override fun onLowMemory() {
+ super.onLowMemory()
+ binding.mapView.onLowMemory()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ binding.mapView.onDestroy()
+ mapboxMap = null
+ locationComponent = null
+ inputJob = null
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt
new file mode 100644
index 0000000..fbb5b6c
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.features.location
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Bundle
+import android.util.AttributeSet
+import android.view.MotionEvent
+import com.mapbox.mapboxsdk.maps.MapView
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback
+
+class FakeLocationMapView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : MapView(context, attrs, defStyleAttr) {
+
+ /**
+ * Overrides onTouchEvent because this MapView is part of a scroll view
+ * and we want this map view to consume all touch events originating on this view.
+ */
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ when (event?.action) {
+ MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true)
+ MotionEvent.ACTION_UP -> parent.requestDisallowInterceptTouchEvent(false)
+ }
+ super.onTouchEvent(event)
+ return true
+ }
+}
+
+fun FakeLocationMapView.setup(savedInstanceState: Bundle?, callback: OnMapReadyCallback) =
+ this.apply {
+ onCreate(savedInstanceState)
+ getMapAsync(callback)
+ }
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt
new file mode 100644
index 0000000..baa672b
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.features.location
+
+import android.location.Location
+import foundation.e.advancedprivacy.domain.entities.LocationMode
+
+data class FakeLocationState(
+ val mode: LocationMode = LocationMode.REAL_LOCATION,
+ val currentLocation: Location? = null,
+ val specificLatitude: Float? = null,
+ val specificLongitude: Float? = null,
+ val forceRefresh: Boolean = false,
+)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt
new file mode 100644
index 0000000..87b64c5
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.features.location
+
+import android.location.Location
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import foundation.e.advancedprivacy.domain.entities.LocationMode
+import foundation.e.advancedprivacy.domain.usecases.FakeLocationStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.time.Duration.Companion.milliseconds
+
+class FakeLocationViewModel(
+ private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
+ private val fakeLocationStateUseCase: FakeLocationStateUseCase
+) : ViewModel() {
+ companion object {
+ private val SET_SPECIFIC_LOCATION_DELAY = 200.milliseconds
+ }
+
+ private val _state = MutableStateFlow(FakeLocationState())
+ val state = _state.asStateFlow()
+
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
+ private val specificLocationInputFlow = MutableSharedFlow<Action.SetSpecificLocationAction>()
+
+ @OptIn(FlowPreview::class)
+ suspend fun doOnStartedState() = withContext(Dispatchers.Main) {
+ launch {
+ merge(
+ fakeLocationStateUseCase.configuredLocationMode.map { (mode, lat, lon) ->
+ _state.update { s ->
+ s.copy(
+ mode = mode,
+ specificLatitude = lat,
+ specificLongitude = lon
+ )
+ }
+ },
+ specificLocationInputFlow
+ .debounce(SET_SPECIFIC_LOCATION_DELAY).map { action ->
+ fakeLocationStateUseCase.setSpecificLocation(action.latitude, action.longitude)
+ }
+ ).collect {}
+ }
+
+ launch {
+ fakeLocationStateUseCase.currentLocation.collect { location ->
+ _singleEvents.emit(
+ SingleEvent.LocationUpdatedEvent(
+ mode = _state.value.mode,
+ location = location
+ )
+ )
+ }
+ }
+ }
+
+ fun submitAction(action: Action) = viewModelScope.launch {
+ when (action) {
+ is Action.StartListeningLocation -> actionStartListeningLocation()
+ is Action.StopListeningLocation -> fakeLocationStateUseCase.stopListeningLocation()
+ is Action.SetSpecificLocationAction -> setSpecificLocation(action)
+ is Action.UseRandomLocationAction -> fakeLocationStateUseCase.setRandomLocation()
+ is Action.UseRealLocationAction ->
+ fakeLocationStateUseCase.stopFakeLocation()
+ }
+ }
+
+ private suspend fun actionStartListeningLocation() {
+ val started = fakeLocationStateUseCase.startListeningLocation()
+ if (!started) {
+ _singleEvents.emit(SingleEvent.RequestLocationPermission)
+ }
+ }
+
+ private suspend fun setSpecificLocation(action: Action.SetSpecificLocationAction) {
+ specificLocationInputFlow.emit(action)
+ }
+
+ sealed class SingleEvent {
+ data class LocationUpdatedEvent(val mode: LocationMode, val location: Location?) : SingleEvent()
+ object RequestLocationPermission : SingleEvent()
+ data class ErrorEvent(val error: String) : SingleEvent()
+ }
+
+ sealed class Action {
+ object StartListeningLocation : Action()
+ object StopListeningLocation : Action()
+ object UseRealLocationAction : Action()
+ object UseRandomLocationAction : Action()
+ data class SetSpecificLocationAction(
+ val latitude: Float,
+ val longitude: Float
+ ) : Action()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt
new file mode 100644
index 0000000..3e17334
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION, 2022 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.features.trackers
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.UnderlineSpan
+import android.view.View
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.fragment.app.commit
+import androidx.fragment.app.replace
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import foundation.e.advancedprivacy.AdvancedPrivacyApplication
+import foundation.e.advancedprivacy.DependencyContainer
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.AppsAdapter
+import foundation.e.advancedprivacy.common.GraphHolder
+import foundation.e.advancedprivacy.common.NavToolbarFragment
+import foundation.e.advancedprivacy.common.setToolTipForAsterisk
+import foundation.e.advancedprivacy.databinding.FragmentTrackersBinding
+import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding
+import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics
+import foundation.e.advancedprivacy.features.trackers.apptrackers.AppTrackersFragment
+import kotlinx.coroutines.launch
+
+class TrackersFragment :
+ NavToolbarFragment(R.layout.fragment_trackers) {
+
+ private val dependencyContainer: DependencyContainer by lazy {
+ (this.requireActivity().application as AdvancedPrivacyApplication).dependencyContainer
+ }
+
+ private val viewModel: TrackersViewModel by viewModels { dependencyContainer.viewModelsFactory }
+
+ private var _binding: FragmentTrackersBinding? = null
+ private val binding get() = _binding!!
+
+ private var dayGraphHolder: GraphHolder? = null
+ private var monthGraphHolder: GraphHolder? = null
+ private var yearGraphHolder: GraphHolder? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ _binding = FragmentTrackersBinding.bind(view)
+
+ dayGraphHolder = GraphHolder(binding.graphDay.graph, requireContext(), false)
+ monthGraphHolder = GraphHolder(binding.graphMonth.graph, requireContext(), false)
+ yearGraphHolder = GraphHolder(binding.graphYear.graph, requireContext(), false)
+
+ binding.apps.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ setHasFixedSize(true)
+ adapter = AppsAdapter(R.layout.trackers_item_app) { appUid ->
+ viewModel.submitAction(
+ TrackersViewModel.Action.ClickAppAction(appUid)
+ )
+ }
+ }
+
+ val infoText = getString(R.string.trackers_info)
+ val moreText = getString(R.string.trackers_info_more)
+
+ val spannable = SpannableString("$infoText $moreText")
+ val startIndex = infoText.length + 1
+ val endIndex = spannable.length
+ spannable.setSpan(
+ ForegroundColorSpan(ContextCompat.getColor(requireContext(), R.color.accent)),
+ startIndex,
+ endIndex,
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+ )
+ spannable.setSpan(UnderlineSpan(), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
+ spannable.setSpan(
+ object : ClickableSpan() {
+ override fun onClick(p0: View) {
+ viewModel.submitAction(TrackersViewModel.Action.ClickLearnMore)
+ }
+ },
+ startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+ )
+
+ with(binding.trackersInfo) {
+ linksClickable = true
+ isClickable = true
+ movementMethod = LinkMovementMethod.getInstance()
+ text = spannable
+ }
+
+ setToolTipForAsterisk(
+ textView = binding.trackersAppsListTitle,
+ textId = R.string.trackers_applist_title,
+ tooltipTextId = R.string.trackers_applist_infos
+ )
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ when (event) {
+ is TrackersViewModel.SingleEvent.ErrorEvent -> {
+ displayToast(event.error)
+ }
+ is TrackersViewModel.SingleEvent.OpenAppDetailsEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<AppTrackersFragment>(
+ R.id.container,
+ args = AppTrackersFragment.buildArgs(
+ event.appDesc.label.toString(),
+ event.appDesc.packageName,
+ event.appDesc.uid
+ )
+ )
+ setReorderingAllowed(true)
+ addToBackStack("apptrackers")
+ }
+ }
+ is TrackersViewModel.SingleEvent.OpenUrl -> {
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, event.url))
+ } catch (e: ActivityNotFoundException) {
+ Toast.makeText(
+ requireContext(),
+ R.string.error_no_activity_view_url,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
+ }
+
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
+ }
+
+ override fun getTitle() = getString(R.string.trackers_title)
+
+ private fun render(state: TrackersState) {
+ state.dayStatistics?.let { renderGraph(it, dayGraphHolder!!, binding.graphDay) }
+ state.monthStatistics?.let { renderGraph(it, monthGraphHolder!!, binding.graphMonth) }
+ state.yearStatistics?.let { renderGraph(it, yearGraphHolder!!, binding.graphYear) }
+
+ state.apps?.let {
+ binding.apps.post {
+ (binding.apps.adapter as AppsAdapter?)?.dataSet = it
+ }
+ }
+ }
+
+ private fun renderGraph(
+ statistics: TrackersPeriodicStatistics,
+ graphHolder: GraphHolder,
+ graphBinding: TrackersItemGraphBinding
+ ) {
+ if (statistics.callsBlockedNLeaked.all { it.first == 0 && it.second == 0 }) {
+ graphBinding.graph.visibility = View.INVISIBLE
+ graphBinding.graphEmpty.isVisible = true
+ } else {
+ graphBinding.graph.isVisible = true
+ graphBinding.graphEmpty.isVisible = false
+ graphHolder.data = statistics.callsBlockedNLeaked
+ graphHolder.labels = statistics.periods
+ graphBinding.trackersCountLabel.text =
+ getString(R.string.trackers_count_label, statistics.trackersCount)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ dayGraphHolder = null
+ monthGraphHolder = null
+ yearGraphHolder = null
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt
new file mode 100644
index 0000000..13719e4
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.features.trackers
+
+import foundation.e.advancedprivacy.domain.entities.AppWithCounts
+import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics
+
+data class TrackersState(
+ val dayStatistics: TrackersPeriodicStatistics? = null,
+ val monthStatistics: TrackersPeriodicStatistics? = null,
+ val yearStatistics: TrackersPeriodicStatistics? = null,
+ val apps: List<AppWithCounts>? = null,
+)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt
new file mode 100644
index 0000000..bcb4df8
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION, 2022 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.features.trackers
+
+import android.net.Uri
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import foundation.e.advancedprivacy.domain.entities.AppWithCounts
+import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class TrackersViewModel(
+ private val trackersStatisticsUseCase: TrackersStatisticsUseCase
+) : ViewModel() {
+
+ companion object {
+ private const val URL_LEARN_MORE_ABOUT_TRACKERS =
+ "https://doc.e.foundation/support-topics/advanced_privacy#trackers-blocker"
+ }
+
+ private val _state = MutableStateFlow(TrackersState())
+ val state = _state.asStateFlow()
+
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ merge(
+ trackersStatisticsUseCase.listenUpdates().map {
+ trackersStatisticsUseCase.getDayMonthYearStatistics()
+ .let { (day, month, year) ->
+ _state.update { s ->
+ s.copy(
+ dayStatistics = day,
+ monthStatistics = month,
+ yearStatistics = year
+ )
+ }
+ }
+ },
+ trackersStatisticsUseCase.getAppsWithCounts().map {
+ _state.update { s -> s.copy(apps = it) }
+ }
+ ).collect {}
+ }
+
+ fun submitAction(action: Action) = viewModelScope.launch {
+ when (action) {
+ is Action.ClickAppAction -> actionClickApp(action)
+ is Action.ClickLearnMore ->
+ _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
+ }
+ }
+
+ private suspend fun actionClickApp(action: Action.ClickAppAction) {
+ state.value.apps?.find { it.uid == action.appUid }?.let {
+ _singleEvents.emit(SingleEvent.OpenAppDetailsEvent(it))
+ }
+ }
+
+ sealed class SingleEvent {
+ data class ErrorEvent(val error: String) : SingleEvent()
+ data class OpenAppDetailsEvent(val appDesc: AppWithCounts) : SingleEvent()
+ data class OpenUrl(val url: Uri) : SingleEvent()
+ }
+
+ sealed class Action {
+ data class ClickAppAction(val appUid: Int) : Action()
+ object ClickLearnMore : Action()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt
new file mode 100644
index 0000000..2bb53d6
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 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.features.trackers.apptrackers
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.core.os.bundleOf
+import androidx.core.view.isVisible
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.snackbar.Snackbar
+import foundation.e.advancedprivacy.AdvancedPrivacyApplication
+import foundation.e.advancedprivacy.DependencyContainer
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.NavToolbarFragment
+import foundation.e.advancedprivacy.databinding.ApptrackersFragmentBinding
+import kotlinx.coroutines.launch
+
+class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) {
+ companion object {
+ private val PARAM_LABEL = "PARAM_LABEL"
+ private val PARAM_PACKAGE_NAME = "PARAM_PACKAGE_NAME"
+
+ const val PARAM_APP_UID = "PARAM_APP_UID"
+
+ fun buildArgs(label: String, packageName: String, appUid: Int): Bundle = bundleOf(
+ PARAM_LABEL to label,
+ PARAM_PACKAGE_NAME to packageName,
+ PARAM_APP_UID to appUid
+ )
+ }
+
+ private val dependencyContainer: DependencyContainer by lazy {
+ (this.requireActivity().application as AdvancedPrivacyApplication).dependencyContainer
+ }
+
+ private val viewModel: AppTrackersViewModel by viewModels {
+ dependencyContainer.viewModelsFactory
+ }
+
+ private var _binding: ApptrackersFragmentBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (arguments == null ||
+ requireArguments().getInt(PARAM_APP_UID, Int.MIN_VALUE) == Int.MIN_VALUE
+ ) {
+ activity?.supportFragmentManager?.popBackStack()
+ }
+ }
+
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
+ }
+
+ override fun getTitle(): String = requireArguments().getString(PARAM_LABEL) ?: ""
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = ApptrackersFragmentBinding.bind(view)
+
+ binding.blockAllToggle.setOnClickListener {
+ viewModel.submitAction(AppTrackersViewModel.Action.BlockAllToggleAction(binding.blockAllToggle.isChecked))
+ }
+ binding.btnReset.setOnClickListener {
+ viewModel.submitAction(AppTrackersViewModel.Action.ResetAllTrackers)
+ }
+
+ binding.trackers.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ setHasFixedSize(true)
+ adapter = ToggleTrackersAdapter(
+ R.layout.apptrackers_item_tracker_toggle,
+ onToggleSwitch = { tracker, isBlocked ->
+ viewModel.submitAction(AppTrackersViewModel.Action.ToggleTrackerAction(tracker, isBlocked))
+ },
+ onClickTitle = { viewModel.submitAction(AppTrackersViewModel.Action.ClickTracker(it)) },
+ )
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ when (event) {
+ is AppTrackersViewModel.SingleEvent.ErrorEvent ->
+ displayToast(getString(event.errorResId))
+ is AppTrackersViewModel.SingleEvent.OpenUrl ->
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, event.url))
+ } catch (e: ActivityNotFoundException) {
+ Toast.makeText(
+ requireContext(),
+ R.string.error_no_activity_view_url,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is AppTrackersViewModel.SingleEvent.ToastTrackersControlDisabled ->
+ Snackbar.make(
+ binding.root,
+ R.string.apptrackers_tracker_control_disabled_message,
+ Snackbar.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
+ }
+ }
+
+ private fun render(state: AppTrackersState) {
+ binding.trackersCountSummary.text = if (state.getTrackersCount() == 0) ""
+ else getString(
+ R.string.apptrackers_trackers_count_summary,
+ state.getBlockedTrackersCount(),
+ state.getTrackersCount(),
+ state.blocked,
+ state.leaked
+ )
+
+ binding.blockAllToggle.isChecked = state.isBlockingActivated
+
+ val trackersStatus = state.getTrackersStatus()
+ if (!trackersStatus.isNullOrEmpty()) {
+ binding.trackersListTitle.isVisible = state.isBlockingActivated
+ binding.trackers.isVisible = true
+ binding.trackers.post {
+ (binding.trackers.adapter as ToggleTrackersAdapter?)?.updateDataSet(
+ trackersStatus,
+ state.isBlockingActivated
+ )
+ }
+ binding.noTrackersYet.isVisible = false
+ binding.btnReset.isVisible = true
+ } else {
+ binding.trackersListTitle.isVisible = false
+ binding.trackers.isVisible = false
+ binding.noTrackersYet.isVisible = true
+ binding.noTrackersYet.text = getString(
+ when {
+ !state.isBlockingActivated -> R.string.apptrackers_no_trackers_yet_block_off
+ state.isWhitelistEmpty -> R.string.apptrackers_no_trackers_yet_block_on
+ else -> R.string.app_trackers_no_trackers_yet_remaining_whitelist
+ }
+ )
+ binding.btnReset.isVisible = state.isBlockingActivated && !state.isWhitelistEmpty
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt
new file mode 100644
index 0000000..2a9e6e8
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.features.trackers.apptrackers
+
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import foundation.e.privacymodules.trackers.api.Tracker
+
+data class AppTrackersState(
+ val appDesc: ApplicationDescription? = null,
+ val isBlockingActivated: Boolean = false,
+ val trackersWithWhiteList: List<Pair<Tracker, Boolean>>? = null,
+ val leaked: Int = 0,
+ val blocked: Int = 0,
+ val isTrackersBlockingEnabled: Boolean = false,
+ val isWhitelistEmpty: Boolean = true,
+ val showQuickPrivacyDisabledMessage: Boolean = false,
+) {
+ fun getTrackersStatus(): List<Pair<Tracker, Boolean>>? {
+ return trackersWithWhiteList?.map { it.first to !it.second }
+ }
+
+ fun getTrackersCount() = trackersWithWhiteList?.size ?: 0
+ fun getBlockedTrackersCount(): Int = if (isTrackersBlockingEnabled && isBlockingActivated)
+ trackersWithWhiteList?.count { !it.second } ?: 0
+ else 0
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt
new file mode 100644
index 0000000..cda4b4b
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 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.features.trackers.apptrackers
+
+import android.net.Uri
+import androidx.annotation.StringRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import foundation.e.advancedprivacy.domain.entities.TrackerMode
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import foundation.e.privacymodules.trackers.api.Tracker
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class AppTrackersViewModel(
+ private val app: ApplicationDescription,
+ private val trackersStateUseCase: TrackersStateUseCase,
+ private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
+ private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase
+) : ViewModel() {
+ companion object {
+ private const val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/trackers/"
+ }
+
+ private val _state = MutableStateFlow(AppTrackersState())
+ val state = _state.asStateFlow()
+
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update {
+ it.copy(
+ appDesc = app,
+ isBlockingActivated = !trackersStateUseCase.isWhitelisted(app),
+ trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(
+ app
+ ),
+ isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
+ )
+ }
+ }
+ }
+
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ merge(
+ getQuickPrivacyStateUseCase.trackerMode.map {
+ _state.update { s -> s.copy(isTrackersBlockingEnabled = it != TrackerMode.VULNERABLE) }
+ },
+ trackersStatisticsUseCase.listenUpdates().map { fetchStatistics() }
+ ).collect { }
+ }
+
+ fun submitAction(action: Action) = viewModelScope.launch {
+ when (action) {
+ is Action.BlockAllToggleAction -> blockAllToggleAction(action)
+ is Action.ToggleTrackerAction -> toggleTrackerAction(action)
+ is Action.ClickTracker -> actionClickTracker(action)
+ is Action.ResetAllTrackers -> resetAllTrackers()
+ }
+ }
+
+ private suspend fun blockAllToggleAction(action: Action.BlockAllToggleAction) {
+ withContext(Dispatchers.IO) {
+ if (!state.value.isTrackersBlockingEnabled) {
+ _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
+ }
+ trackersStateUseCase.toggleAppWhitelist(app, !action.isBlocked)
+ _state.update {
+ it.copy(
+ isBlockingActivated = !trackersStateUseCase.isWhitelisted(app)
+ )
+ }
+ }
+ }
+
+ private suspend fun toggleTrackerAction(action: Action.ToggleTrackerAction) {
+ withContext(Dispatchers.IO) {
+ if (!state.value.isTrackersBlockingEnabled) {
+ _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
+ }
+
+ if (state.value.isBlockingActivated) {
+ trackersStateUseCase.blockTracker(app, action.tracker, action.isBlocked)
+ updateWhitelist()
+ }
+ }
+ }
+
+ private suspend fun actionClickTracker(action: Action.ClickTracker) {
+ withContext(Dispatchers.IO) {
+ action.tracker.exodusId?.let {
+ try {
+ _singleEvents.emit(
+ SingleEvent.OpenUrl(
+ Uri.parse(exodusBaseUrl + it)
+ )
+ )
+ } catch (e: Exception) {
+ }
+ }
+ }
+ }
+
+ private suspend fun resetAllTrackers() {
+ withContext(Dispatchers.IO) {
+ trackersStateUseCase.clearWhitelist(app)
+ updateWhitelist()
+ }
+ }
+ private fun fetchStatistics() {
+ val (blocked, leaked) = trackersStatisticsUseCase.getCalls(app)
+ return _state.update { s ->
+ s.copy(
+ trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app),
+ leaked = leaked,
+ blocked = blocked,
+ isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
+ )
+ }
+ }
+
+ private fun updateWhitelist() {
+ _state.update { s ->
+ s.copy(
+ trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app),
+ isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
+ )
+ }
+ }
+
+ sealed class SingleEvent {
+ data class ErrorEvent(@StringRes val errorResId: Int) : SingleEvent()
+ data class OpenUrl(val url: Uri) : SingleEvent()
+ object ToastTrackersControlDisabled : SingleEvent()
+ }
+
+ sealed class Action {
+ data class BlockAllToggleAction(val isBlocked: Boolean) : Action()
+ data class ToggleTrackerAction(val tracker: Tracker, val isBlocked: Boolean) : Action()
+ data class ClickTracker(val tracker: Tracker) : Action()
+ object ResetAllTrackers : Action()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt
new file mode 100644
index 0000000..3696939
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.features.trackers.apptrackers
+
+import android.text.SpannableString
+import android.text.style.UnderlineSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Switch
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.privacymodules.trackers.api.Tracker
+
+class ToggleTrackersAdapter(
+ private val itemsLayout: Int,
+ private val onToggleSwitch: (Tracker, Boolean) -> Unit,
+ private val onClickTitle: (Tracker) -> Unit
+) : RecyclerView.Adapter<ToggleTrackersAdapter.ViewHolder>() {
+
+ var isEnabled = true
+
+ class ViewHolder(
+ view: View,
+ private val onToggleSwitch: (Tracker, Boolean) -> Unit,
+ private val onClickTitle: (Tracker) -> Unit
+ ) : RecyclerView.ViewHolder(view) {
+ val title: TextView = view.findViewById(R.id.title)
+
+ val toggle: Switch = view.findViewById(R.id.toggle)
+
+ fun bind(item: Pair<Tracker, Boolean>, isEnabled: Boolean) {
+ val text = item.first.label
+ if (item.first.exodusId != null) {
+ title.setTextColor(ContextCompat.getColor(title.context, R.color.accent))
+ val spannable = SpannableString(text)
+ spannable.setSpan(UnderlineSpan(), 0, spannable.length, 0)
+ title.text = spannable
+ } else {
+ title.setTextColor(ContextCompat.getColor(title.context, R.color.primary_text))
+ title.text = text
+ }
+
+ toggle.isChecked = item.second
+ toggle.isEnabled = isEnabled
+
+ toggle.setOnClickListener {
+ onToggleSwitch(item.first, toggle.isChecked)
+ }
+
+ title.setOnClickListener { onClickTitle(item.first) }
+ }
+ }
+
+ private var dataSet: List<Pair<Tracker, Boolean>> = emptyList()
+
+ fun updateDataSet(new: List<Pair<Tracker, Boolean>>, isEnabled: Boolean) {
+ this.isEnabled = isEnabled
+ dataSet = new
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(itemsLayout, parent, false)
+ return ViewHolder(view, onToggleSwitch, onClickTitle)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val permission = dataSet[position]
+ holder.bind(permission, isEnabled)
+ }
+
+ override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/main/MainActivity.kt b/app/src/main/java/foundation/e/advancedprivacy/main/MainActivity.kt
new file mode 100644
index 0000000..ec33e25
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/main/MainActivity.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.main
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.add
+import androidx.fragment.app.commit
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.features.dashboard.DashboardFragment
+import foundation.e.advancedprivacy.features.internetprivacy.InternetPrivacyFragment
+import foundation.e.advancedprivacy.features.location.FakeLocationFragment
+import foundation.e.advancedprivacy.features.trackers.TrackersFragment
+
+open class MainActivity : FragmentActivity(R.layout.activity_main) {
+ override fun onPostCreate(savedInstanceState: Bundle?) {
+ super.onPostCreate(savedInstanceState)
+ handleIntent(intent)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ handleIntent(intent)
+ }
+
+ open fun handleIntent(intent: Intent) {
+ supportFragmentManager.commit {
+ setReorderingAllowed(true)
+ when (intent.action) {
+ ACTION_HIGHLIGHT_LEAKS -> add<DashboardFragment>(
+ containerViewId = R.id.container,
+ args = intent.extras
+ )
+ ACTION_VIEW_TRACKERS -> {
+ add<TrackersFragment>(R.id.container)
+ }
+ ACTION_VIEW_FAKE_LOCATION -> {
+ add<FakeLocationFragment>(R.id.container)
+ }
+ ACTION_VIEW_IPSCRAMBLING -> {
+ add<InternetPrivacyFragment>(R.id.container)
+ }
+ else -> add<DashboardFragment>(R.id.container)
+ }
+ disallowAddToBackStack()
+ }
+ }
+
+ override fun finishAfterTransition() {
+ val resultData = Intent()
+ val result = onPopulateResultIntent(resultData)
+ setResult(result, resultData)
+
+ super.finishAfterTransition()
+ }
+
+ open fun onPopulateResultIntent(intent: Intent): Int = Activity.RESULT_OK
+
+ companion object {
+ private const val ACTION_HIGHLIGHT_LEAKS = "ACTION_HIGHLIGHT_LEAKS"
+ private const val ACTION_VIEW_TRACKERS = "ACTION_VIEW_TRACKERS"
+ private const val ACTION_VIEW_FAKE_LOCATION = "ACTION_VIEW_FAKE_LOCATION"
+ private const val ACTION_VIEW_IPSCRAMBLING = "ACTION_VIEW_IPSCRAMBLING"
+
+ fun createHighlightLeaksIntent(context: Context, highlightIndex: Int) =
+ Intent(context, MainActivity::class.java).apply {
+ action = ACTION_HIGHLIGHT_LEAKS
+ putExtras(DashboardFragment.buildArgs(highlightIndex))
+ }
+
+ fun createTrackersIntent(context: Context) =
+ Intent(context, MainActivity::class.java).apply {
+ action = ACTION_VIEW_TRACKERS
+ }
+
+ fun createFakeLocationIntent(context: Context): Intent {
+ return Intent(context, MainActivity::class.java).apply {
+ action = ACTION_VIEW_FAKE_LOCATION
+ }
+ }
+
+ fun createIpScramblingIntent(context: Context): Intent {
+ return Intent(context, MainActivity::class.java).apply {
+ action = ACTION_VIEW_IPSCRAMBLING
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt
new file mode 100644
index 0000000..a4272e2
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt
@@ -0,0 +1,156 @@
+/*
+ * 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
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.Context
+import android.os.Bundle
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.advancedprivacy.widget.State
+import foundation.e.advancedprivacy.widget.render
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.sample
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import java.time.temporal.ChronoUnit
+
+/**
+ * Implementation of App Widget functionality.
+ */
+class Widget : AppWidgetProvider() {
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ render(context, state.value, appWidgetManager)
+ }
+
+ override fun onEnabled(context: Context) {
+ // Enter relevant functionality for when the first widget is created
+ }
+
+ override fun onDisabled(context: Context) {
+ // Enter relevant functionality for when the last widget is disabled
+ }
+
+ companion object {
+ private var updateWidgetJob: Job? = null
+
+ private var state: StateFlow<State> = MutableStateFlow(State())
+
+ private const val DARK_TEXT_KEY = "foundation.e.blisslauncher.WIDGET_OPTION_DARK_TEXT"
+ var isDarkText = false
+
+ @OptIn(FlowPreview::class)
+ private fun initState(
+ getPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
+ trackersStatisticsUseCase: TrackersStatisticsUseCase,
+ coroutineScope: CoroutineScope
+ ): StateFlow<State> {
+
+ return combine(
+ getPrivacyStateUseCase.quickPrivacyState,
+ getPrivacyStateUseCase.trackerMode,
+ getPrivacyStateUseCase.isLocationHidden,
+ getPrivacyStateUseCase.ipScramblingMode,
+ ) { quickPrivacyState, trackerMode, isLocationHidden, ipScramblingMode ->
+
+ State(
+ quickPrivacyState = quickPrivacyState,
+ trackerMode = trackerMode,
+ isLocationHidden = isLocationHidden,
+ ipScramblingMode = ipScramblingMode
+ )
+ }.sample(50)
+ .combine(
+ merge(
+ trackersStatisticsUseCase.listenUpdates()
+ .onStart { emit(Unit) }
+ .debounce(5000),
+ flow {
+ while (true) {
+ emit(Unit)
+ delay(ChronoUnit.HOURS.duration.toMillis())
+ }
+ }
+
+ )
+ ) { state, _ ->
+ state.copy(
+ dayStatistics = trackersStatisticsUseCase.getDayTrackersCalls(),
+ activeTrackersCount = trackersStatisticsUseCase.getDayTrackersCount()
+ )
+ }.stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.Eagerly,
+ initialValue = State()
+ )
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ fun startListening(
+ appContext: Context,
+ getPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
+ trackersStatisticsUseCase: TrackersStatisticsUseCase,
+ ) {
+ state = initState(
+ getPrivacyStateUseCase,
+ trackersStatisticsUseCase,
+ GlobalScope
+ )
+
+ updateWidgetJob?.cancel()
+ updateWidgetJob = GlobalScope.launch(Dispatchers.Main) {
+ state.collect {
+ render(appContext, it, AppWidgetManager.getInstance(appContext))
+ }
+ }
+ }
+ }
+
+ override fun onAppWidgetOptionsChanged(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetId: Int,
+ newOptions: Bundle?
+ ) {
+ super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
+ if (newOptions != null) {
+ isDarkText = newOptions.getBoolean(DARK_TEXT_KEY)
+ }
+ render(context, state.value, appWidgetManager)
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt
new file mode 100644
index 0000000..f68a59c
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.widget
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import foundation.e.advancedprivacy.AdvancedPrivacyApplication
+
+class WidgetCommandReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val getQuickPrivacyStateUseCase = (context?.applicationContext as? AdvancedPrivacyApplication)?.dependencyContainer?.getQuickPrivacyStateUseCase
+
+ when (intent?.action) {
+ ACTION_TOGGLE_TRACKERS -> getQuickPrivacyStateUseCase?.toggleTrackers()
+ ACTION_TOGGLE_LOCATION -> getQuickPrivacyStateUseCase?.toggleLocation()
+ ACTION_TOGGLE_IPSCRAMBLING -> getQuickPrivacyStateUseCase?.toggleIpScrambling()
+ else -> {}
+ }
+ }
+
+ companion object {
+ const val ACTION_TOGGLE_TRACKERS = "toggle_trackers"
+ const val ACTION_TOGGLE_LOCATION = "toggle_location"
+ const val ACTION_TOGGLE_IPSCRAMBLING = "toggle_ipscrambling"
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt
new file mode 100644
index 0000000..cb7fe5c
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt
@@ -0,0 +1,381 @@
+/*
+ * 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.widget
+
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_IMMUTABLE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.view.View
+import android.widget.RemoteViews
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.Widget
+import foundation.e.advancedprivacy.Widget.Companion.isDarkText
+import foundation.e.advancedprivacy.common.extensions.dpToPxF
+import foundation.e.advancedprivacy.domain.entities.InternetPrivacyMode
+import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState
+import foundation.e.advancedprivacy.domain.entities.TrackerMode
+import foundation.e.advancedprivacy.main.MainActivity
+import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_IPSCRAMBLING
+import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_LOCATION
+import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_TRACKERS
+
+data class State(
+ val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED,
+ val trackerMode: TrackerMode = TrackerMode.VULNERABLE,
+ val isLocationHidden: Boolean = false,
+ val ipScramblingMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP_LOADING,
+ val dayStatistics: List<Pair<Int, Int>> = emptyList(),
+ val activeTrackersCount: Int = 0,
+)
+
+fun render(
+ context: Context,
+ state: State,
+ appWidgetManager: AppWidgetManager,
+) {
+ val views = RemoteViews(context.packageName, R.layout.widget)
+ applyDarkText(context, state, views)
+ views.apply {
+ val openPIntent = PendingIntent.getActivity(
+ context,
+ REQUEST_CODE_DASHBOARD,
+ Intent(context, MainActivity::class.java),
+ FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
+ )
+ setOnClickPendingIntent(R.id.settings_btn, openPIntent)
+ setOnClickPendingIntent(R.id.widget_container, openPIntent)
+
+ setTextViewText(
+ R.id.state_label,
+ context.getString(
+ when (state.quickPrivacyState) {
+ QuickPrivacyState.DISABLED -> R.string.widget_state_title_off
+ QuickPrivacyState.FULL_ENABLED -> R.string.widget_state_title_on
+ QuickPrivacyState.ENABLED -> R.string.widget_state_title_custom
+ }
+ )
+ )
+
+ setImageViewResource(
+ R.id.toggle_trackers,
+ if (state.trackerMode == TrackerMode.VULNERABLE)
+ R.drawable.ic_switch_disabled
+ else R.drawable.ic_switch_enabled
+ )
+
+ setOnClickPendingIntent(
+ R.id.toggle_trackers,
+ PendingIntent.getBroadcast(
+ context,
+ REQUEST_CODE_TOGGLE_TRACKERS,
+ Intent(context, WidgetCommandReceiver::class.java).apply {
+ action = ACTION_TOGGLE_TRACKERS
+ },
+ FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
+ )
+ )
+
+ setTextViewText(
+ R.id.state_trackers,
+ context.getString(
+ when (state.trackerMode) {
+ TrackerMode.DENIED -> R.string.widget_state_trackers_on
+ TrackerMode.VULNERABLE -> R.string.widget_state_trackers_off
+ TrackerMode.CUSTOM -> R.string.widget_state_trackers_custom
+ }
+ )
+ )
+
+ setImageViewResource(
+ R.id.toggle_location,
+ if (state.isLocationHidden) R.drawable.ic_switch_enabled
+ else R.drawable.ic_switch_disabled
+ )
+
+ setOnClickPendingIntent(
+ R.id.toggle_location,
+ PendingIntent.getBroadcast(
+ context,
+ REQUEST_CODE_TOGGLE_LOCATION,
+ Intent(context, WidgetCommandReceiver::class.java).apply {
+ action = ACTION_TOGGLE_LOCATION
+ },
+ FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
+ )
+ )
+
+ setTextViewText(
+ R.id.state_geolocation,
+ context.getString(
+ if (state.isLocationHidden) R.string.widget_state_geolocation_on
+ else R.string.widget_state_geolocation_off
+ )
+ )
+
+ setImageViewResource(
+ R.id.toggle_ipscrambling,
+ if (state.ipScramblingMode.isChecked) R.drawable.ic_switch_enabled
+ else R.drawable.ic_switch_disabled
+ )
+
+ setOnClickPendingIntent(
+ R.id.toggle_ipscrambling,
+ PendingIntent.getBroadcast(
+ context,
+ REQUEST_CODE_TOGGLE_IPSCRAMBLING,
+ Intent(context, WidgetCommandReceiver::class.java).apply {
+ action = ACTION_TOGGLE_IPSCRAMBLING
+ },
+ FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
+ )
+ )
+
+ setTextViewText(
+ R.id.state_ip_address,
+ context.getString(
+ if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.widget_state_ipaddress_on
+ else R.string.widget_state_ipaddress_off
+ )
+ )
+
+ val loading = state.ipScramblingMode.isLoading
+
+ setViewVisibility(R.id.state_ip_address, if (loading) View.GONE else View.VISIBLE)
+
+ setViewVisibility(R.id.state_ip_address_loader, if (loading) View.VISIBLE else View.GONE)
+
+ if (state.dayStatistics.all { it.first == 0 && it.second == 0 }) {
+ setViewVisibility(R.id.graph, View.GONE)
+ setViewVisibility(R.id.graph_legend, View.GONE)
+ setViewVisibility(R.id.graph_empty, View.VISIBLE)
+ setViewVisibility(R.id.graph_legend_values, View.GONE)
+ setViewVisibility(R.id.graph_view_trackers_btn, View.GONE)
+ } else {
+ setViewVisibility(R.id.graph, View.VISIBLE)
+ setViewVisibility(R.id.graph_legend, View.VISIBLE)
+ setViewVisibility(R.id.graph_empty, View.GONE)
+ setViewVisibility(R.id.graph_legend_values, View.VISIBLE)
+ setViewVisibility(R.id.graph_view_trackers_btn, View.VISIBLE)
+
+ val pIntent = PendingIntent.getActivity(
+ context,
+ REQUEST_CODE_TRACKERS,
+ MainActivity.createTrackersIntent(context),
+ FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
+ )
+
+ setOnClickPendingIntent(R.id.graph_view_trackers_btn, pIntent)
+
+ val graphHeightPx = 26.dpToPxF(context)
+ val maxValue =
+ state.dayStatistics
+ .map { it.first + it.second }
+ .maxOrNull()
+ .let { if (it == null || it == 0) 1 else it }
+ val ratio = graphHeightPx / maxValue
+
+ state.dayStatistics.forEachIndexed { index, (blocked, leaked) ->
+ // blocked (the bar below)
+ val middlePadding = graphHeightPx - blocked * ratio
+ setViewPadding(blockedBarIds[index], 0, middlePadding.toInt(), 0, 0)
+
+ // leaked (the bar above)
+ val topPadding = graphHeightPx - (blocked + leaked) * ratio
+ setViewPadding(leakedBarIds[index], 0, topPadding.toInt(), 0, 0)
+
+ val highlightPIntent = PendingIntent.getActivity(
+ context, REQUEST_CODE_HIGHLIGHT + index,
+ MainActivity.createHighlightLeaksIntent(context, index),
+ FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
+ )
+ setOnClickPendingIntent(containerBarIds[index], highlightPIntent)
+ }
+
+ setTextViewText(
+ R.id.graph_legend,
+ context.getString(
+ R.string.widget_graph_trackers_legend,
+ state.activeTrackersCount.toString()
+ )
+ )
+ }
+ }
+
+ appWidgetManager.updateAppWidget(ComponentName(context, Widget::class.java), views)
+}
+
+private val containerBarIds = listOf(
+ R.id.widget_graph_bar_container_0,
+ R.id.widget_graph_bar_container_1,
+ R.id.widget_graph_bar_container_2,
+ R.id.widget_graph_bar_container_3,
+ R.id.widget_graph_bar_container_4,
+ R.id.widget_graph_bar_container_5,
+ R.id.widget_graph_bar_container_6,
+ R.id.widget_graph_bar_container_7,
+ R.id.widget_graph_bar_container_8,
+ R.id.widget_graph_bar_container_9,
+ R.id.widget_graph_bar_container_10,
+ R.id.widget_graph_bar_container_11,
+ R.id.widget_graph_bar_container_12,
+ R.id.widget_graph_bar_container_13,
+ R.id.widget_graph_bar_container_14,
+ R.id.widget_graph_bar_container_15,
+ R.id.widget_graph_bar_container_16,
+ R.id.widget_graph_bar_container_17,
+ R.id.widget_graph_bar_container_18,
+ R.id.widget_graph_bar_container_19,
+ R.id.widget_graph_bar_container_20,
+ R.id.widget_graph_bar_container_21,
+ R.id.widget_graph_bar_container_22,
+ R.id.widget_graph_bar_container_23,
+)
+
+private val blockedBarIds = listOf(
+ R.id.widget_graph_bar_0,
+ R.id.widget_graph_bar_1,
+ R.id.widget_graph_bar_2,
+ R.id.widget_graph_bar_3,
+ R.id.widget_graph_bar_4,
+ R.id.widget_graph_bar_5,
+ R.id.widget_graph_bar_6,
+ R.id.widget_graph_bar_7,
+ R.id.widget_graph_bar_8,
+ R.id.widget_graph_bar_9,
+ R.id.widget_graph_bar_10,
+ R.id.widget_graph_bar_11,
+ R.id.widget_graph_bar_12,
+ R.id.widget_graph_bar_13,
+ R.id.widget_graph_bar_14,
+ R.id.widget_graph_bar_15,
+ R.id.widget_graph_bar_16,
+ R.id.widget_graph_bar_17,
+ R.id.widget_graph_bar_18,
+ R.id.widget_graph_bar_19,
+ R.id.widget_graph_bar_20,
+ R.id.widget_graph_bar_21,
+ R.id.widget_graph_bar_22,
+ R.id.widget_graph_bar_23
+)
+
+private val leakedBarIds = listOf(
+ R.id.widget_leaked_graph_bar_0,
+ R.id.widget_leaked_graph_bar_1,
+ R.id.widget_leaked_graph_bar_2,
+ R.id.widget_leaked_graph_bar_3,
+ R.id.widget_leaked_graph_bar_4,
+ R.id.widget_leaked_graph_bar_5,
+ R.id.widget_leaked_graph_bar_6,
+ R.id.widget_leaked_graph_bar_7,
+ R.id.widget_leaked_graph_bar_8,
+ R.id.widget_leaked_graph_bar_9,
+ R.id.widget_leaked_graph_bar_10,
+ R.id.widget_leaked_graph_bar_11,
+ R.id.widget_leaked_graph_bar_12,
+ R.id.widget_leaked_graph_bar_13,
+ R.id.widget_leaked_graph_bar_14,
+ R.id.widget_leaked_graph_bar_15,
+ R.id.widget_leaked_graph_bar_16,
+ R.id.widget_leaked_graph_bar_17,
+ R.id.widget_leaked_graph_bar_18,
+ R.id.widget_leaked_graph_bar_19,
+ R.id.widget_leaked_graph_bar_20,
+ R.id.widget_leaked_graph_bar_21,
+ R.id.widget_leaked_graph_bar_22,
+ R.id.widget_leaked_graph_bar_23
+)
+
+private const val REQUEST_CODE_DASHBOARD = 1
+private const val REQUEST_CODE_TRACKERS = 3
+private const val REQUEST_CODE_TOGGLE_TRACKERS = 4
+private const val REQUEST_CODE_TOGGLE_LOCATION = 5
+private const val REQUEST_CODE_TOGGLE_IPSCRAMBLING = 6
+private const val REQUEST_CODE_HIGHLIGHT = 100
+
+fun applyDarkText(context: Context, state: State, views: RemoteViews) {
+ views.apply {
+ listOf(
+ R.id.state_label,
+ R.id.graph_legend_blocked,
+ R.id.graph_legend_allowed,
+
+ )
+ .forEach {
+ setTextColor(
+ it,
+ context.getColor(if (isDarkText) R.color.on_surface_disabled_light else R.color.on_primary_medium_emphasis)
+ )
+ }
+ setTextColor(
+ R.id.widget_title,
+ context.getColor(if (isDarkText) R.color.on_surface_medium_emphasis_light else R.color.on_surface_high_emphasis)
+ )
+ listOf(
+ R.id.state_trackers,
+ R.id.state_geolocation,
+ R.id.state_ip_address,
+ R.id.graph_legend,
+ R.id.graph_view_trackers_btn
+ )
+ .forEach {
+ setTextColor(
+ it,
+ context.getColor(if (isDarkText) R.color.on_surface_medium_emphasis_light else R.color.on_primary_high_emphasis)
+ )
+ }
+
+ listOf(
+ R.id.trackers_label,
+ R.id.geolocation_label,
+ R.id.ip_address_label,
+ R.id.graph_empty
+
+ )
+ .forEach {
+ setTextColor(
+ it,
+ context.getColor(if (isDarkText) R.color.on_surface_disabled_light else R.color.on_primary_disabled)
+ )
+ }
+ setTextViewCompoundDrawables(
+ R.id.graph_view_trackers_btn,
+ 0,
+ 0,
+ if (isDarkText) R.drawable.ic_chevron_right_24dp_light else R.drawable.ic_chevron_right_24dp,
+ 0
+ )
+ setImageViewResource(
+ R.id.settings_btn,
+ if (isDarkText) R.drawable.ic_settings_light else R.drawable.ic_settings
+ )
+ setImageViewResource(
+ R.id.state_icon,
+ if (isDarkText) {
+ if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on_light
+ else R.drawable.ic_shield_off_light
+ } else {
+ if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on_white
+ else R.drawable.ic_shield_off_white
+ }
+ )
+ }
+}