From a8874167f663885f2d3371801cf03681576ac817 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Tue, 2 May 2023 21:25:17 +0200 Subject: 1200: rename everything to AdvancedPrivacy --- app/src/main/AndroidManifest.xml | 16 +- .../advancedprivacy/AdvancedPrivacyApplication.kt | 36 ++ .../e/advancedprivacy/DependencyContainer.kt | 211 ++++++++++++ .../foundation/e/advancedprivacy/Notifications.kt | 210 ++++++++++++ .../e/advancedprivacy/UpdateTrackersWorker.kt | 59 ++++ .../e/advancedprivacy/common/AppsAdapter.kt | 71 ++++ .../common/BootCompletedReceiver.kt | 36 ++ .../foundation/e/advancedprivacy/common/Factory.kt | 23 ++ .../e/advancedprivacy/common/GraphHolder.kt | 333 ++++++++++++++++++ .../e/advancedprivacy/common/NavToolbarFragment.kt | 33 ++ .../e/advancedprivacy/common/RightRadioButton.kt | 43 +++ .../e/advancedprivacy/common/TextViewHelpers.kt | 63 ++++ .../e/advancedprivacy/common/ThrottleFlow.kt | 36 ++ .../e/advancedprivacy/common/ToggleAppsAdapter.kt | 76 ++++ .../e/advancedprivacy/common/ToolbarFragment.kt | 45 +++ .../e/advancedprivacy/common/WarningDialog.kt | 130 +++++++ .../common/extensions/AnyExtension.kt | 22 ++ .../data/repositories/AppListsRepository.kt | 281 +++++++++++++++ .../data/repositories/CityDataSource.kt | 46 +++ .../data/repositories/LocalStateRepository.kt | 116 +++++++ .../data/repositories/TrackersRepository.kt | 129 +++++++ .../domain/entities/AppWithCounts.kt | 59 ++++ .../domain/entities/InternetPrivacyMode.kt | 29 ++ .../domain/entities/LocationMode.kt | 22 ++ .../domain/entities/MainFeatures.kt | 22 ++ .../domain/entities/QuickPrivacyState.kt | 24 ++ .../advancedprivacy/domain/entities/TrackerMode.kt | 22 ++ .../domain/entities/TrackersPeriodicStatistics.kt | 25 ++ .../domain/usecases/AppListUseCase.kt | 39 +++ .../domain/usecases/FakeLocationStateUseCase.kt | 209 +++++++++++ .../domain/usecases/GetQuickPrivacyStateUseCase.kt | 89 +++++ .../domain/usecases/IpScramblingStateUseCase.kt | 170 +++++++++ .../domain/usecases/ShowFeaturesWarningUseCase.kt | 54 +++ .../domain/usecases/TrackersStateUseCase.kt | 105 ++++++ .../domain/usecases/TrackersStatisticsUseCase.kt | 278 +++++++++++++++ .../domain/usecases/UpdateWidgetUseCase.kt | 33 ++ .../features/dashboard/DashboardFragment.kt | 307 +++++++++++++++++ .../features/dashboard/DashboardState.kt | 37 ++ .../features/dashboard/DashboardViewModel.kt | 158 +++++++++ .../internetprivacy/InternetPrivacyFragment.kt | 201 +++++++++++ .../internetprivacy/InternetPrivacyState.kt | 36 ++ .../internetprivacy/InternetPrivacyViewModel.kt | 157 +++++++++ .../features/location/FakeLocationFragment.kt | 376 ++++++++++++++++++++ .../features/location/FakeLocationMapView.kt | 53 +++ .../features/location/FakeLocationState.kt | 29 ++ .../features/location/FakeLocationViewModel.kt | 126 +++++++ .../features/trackers/TrackersFragment.kt | 218 ++++++++++++ .../features/trackers/TrackersState.kt | 28 ++ .../features/trackers/TrackersViewModel.kt | 95 +++++ .../trackers/apptrackers/AppTrackersFragment.kt | 189 ++++++++++ .../trackers/apptrackers/AppTrackersState.kt | 42 +++ .../trackers/apptrackers/AppTrackersViewModel.kt | 172 ++++++++++ .../trackers/apptrackers/ToggleTrackersAdapter.kt | 92 +++++ .../e/advancedprivacy/main/MainActivity.kt | 106 ++++++ .../foundation/e/advancedprivacy/widget/Widget.kt | 156 +++++++++ .../widget/WidgetCommandReceiver.kt | 42 +++ .../e/advancedprivacy/widget/WidgetUI.kt | 381 +++++++++++++++++++++ .../e/privacycentralapp/DependencyContainer.kt | 211 ------------ .../e/privacycentralapp/Notifications.kt | 210 ------------ .../privacycentralapp/PrivacyCentralApplication.kt | 36 -- .../e/privacycentralapp/UpdateTrackersWorker.kt | 59 ---- .../e/privacycentralapp/common/AppsAdapter.kt | 71 ---- .../common/BootCompletedReceiver.kt | 36 -- .../e/privacycentralapp/common/Factory.kt | 23 -- .../e/privacycentralapp/common/GraphHolder.kt | 333 ------------------ .../privacycentralapp/common/NavToolbarFragment.kt | 33 -- .../e/privacycentralapp/common/RightRadioButton.kt | 43 --- .../e/privacycentralapp/common/TextViewHelpers.kt | 63 ---- .../e/privacycentralapp/common/ThrottleFlow.kt | 36 -- .../privacycentralapp/common/ToggleAppsAdapter.kt | 76 ---- .../e/privacycentralapp/common/ToolbarFragment.kt | 45 --- .../e/privacycentralapp/common/WarningDialog.kt | 130 ------- .../common/extensions/AnyExtension.kt | 22 -- .../data/repositories/AppListsRepository.kt | 281 --------------- .../data/repositories/CityDataSource.kt | 46 --- .../data/repositories/LocalStateRepository.kt | 116 ------- .../data/repositories/TrackersRepository.kt | 129 ------- .../domain/entities/AppWithCounts.kt | 59 ---- .../domain/entities/InternetPrivacyMode.kt | 29 -- .../domain/entities/LocationMode.kt | 22 -- .../domain/entities/MainFeatures.kt | 22 -- .../domain/entities/QuickPrivacyState.kt | 24 -- .../domain/entities/TrackerMode.kt | 22 -- .../domain/entities/TrackersPeriodicStatistics.kt | 25 -- .../domain/usecases/AppListUseCase.kt | 39 --- .../domain/usecases/FakeLocationStateUseCase.kt | 209 ----------- .../domain/usecases/GetQuickPrivacyStateUseCase.kt | 89 ----- .../domain/usecases/IpScramblingStateUseCase.kt | 170 --------- .../domain/usecases/ShowFeaturesWarningUseCase.kt | 54 --- .../domain/usecases/TrackersStateUseCase.kt | 105 ------ .../domain/usecases/TrackersStatisticsUseCase.kt | 278 --------------- .../domain/usecases/UpdateWidgetUseCase.kt | 33 -- .../features/dashboard/DashboardFragment.kt | 307 ----------------- .../features/dashboard/DashboardState.kt | 37 -- .../features/dashboard/DashboardViewModel.kt | 158 --------- .../internetprivacy/InternetPrivacyFragment.kt | 201 ----------- .../internetprivacy/InternetPrivacyState.kt | 36 -- .../internetprivacy/InternetPrivacyViewModel.kt | 157 --------- .../features/location/FakeLocationFragment.kt | 376 -------------------- .../features/location/FakeLocationMapView.kt | 53 --- .../features/location/FakeLocationState.kt | 29 -- .../features/location/FakeLocationViewModel.kt | 126 ------- .../features/trackers/TrackersFragment.kt | 218 ------------ .../features/trackers/TrackersState.kt | 28 -- .../features/trackers/TrackersViewModel.kt | 95 ----- .../trackers/apptrackers/AppTrackersFragment.kt | 189 ---------- .../trackers/apptrackers/AppTrackersState.kt | 42 --- .../trackers/apptrackers/AppTrackersViewModel.kt | 172 ---------- .../trackers/apptrackers/ToggleTrackersAdapter.kt | 92 ----- .../e/privacycentralapp/main/MainActivity.kt | 106 ------ .../e/privacycentralapp/widget/Widget.kt | 156 --------- .../widget/WidgetCommandReceiver.kt | 42 --- .../e/privacycentralapp/widget/WidgetUI.kt | 381 --------------------- app/src/main/res/layout/fragment_fake_location.xml | 8 +- .../res/layout/ipscrambling_item_selectmode.xml | 2 +- app/src/main/res/values/themes.xml | 4 +- .../e/advancedprivacy/ExampleUnitTest.kt | 33 ++ .../e/privacycentralapp/ExampleUnitTest.kt | 33 -- 118 files changed, 6228 insertions(+), 6228 deletions(-) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/Notifications.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/UpdateTrackersWorker.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/data/repositories/CityDataSource.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/entities/AppWithCounts.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/entities/InternetPrivacyMode.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/entities/MainFeatures.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/entities/QuickPrivacyState.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackerMode.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppListUseCase.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/IpScramblingStateUseCase.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/UpdateWidgetUseCase.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyFragment.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyState.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/internetprivacy/InternetPrivacyViewModel.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/main/MainActivity.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/widget/WidgetCommandReceiver.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/Notifications.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/BootCompletedReceiver.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/Factory.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/NavToolbarFragment.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/RightRadioButton.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/TextViewHelpers.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/WarningDialog.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/data/repositories/CityDataSource.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/entities/InternetPrivacyMode.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/entities/LocationMode.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/entities/MainFeatures.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/entities/QuickPrivacyState.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackerMode.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/usecases/ShowFeaturesWarningUseCase.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt delete mode 100644 app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt create mode 100644 app/src/test/java/foundation/e/advancedprivacy/ExampleUnitTest.kt delete mode 100644 app/src/test/java/foundation/e/privacycentralapp/ExampleUnitTest.kt (limited to 'app') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c155aa5..246b7c0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ --> @@ -40,15 +40,15 @@ android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:name=".PrivacyCentralApplication" + android:name="foundation.e.advancedprivacy.AdvancedPrivacyApplication" android:persistent="${persistent}" android:supportsRtl="true" - android:theme="@style/Theme.PrivacyCentralApp" + android:theme="@style/Theme.AdvancedPrivacy" android:windowSoftInputMode="adjustResize" tools:replace="android:icon,android:label,android:theme" > @@ -59,7 +59,7 @@ @@ -70,14 +70,14 @@ android:resource="@xml/widget_info" /> - - @@ -87,7 +87,7 @@ . + */ + +package foundation.e.advancedprivacy + +import android.app.Application +import com.mapbox.mapboxsdk.Mapbox +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) + Mapbox.getTelemetry()?.setUserTelemetryRequestState(false) + + 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 . + */ + +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 create(modelClass: Class, 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 . + */ + +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 . + */ + +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( + 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 . + */ + +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() { + + 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 = 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 . + */ + +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 . + */ + +package foundation.e.advancedprivacy.common + +// Definition of a Factory interface with a function to create objects of a type +interface Factory { + 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 . + */ + +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>() + set(value) { + field = value + refreshDataSet() + } + var labels = emptyList() + + var graduations: List? = 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(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(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 . + */ + +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 . + */ + +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 . + */ + +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 . + */ + +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 Flow.throttleFirst(windowDuration: Duration): Flow = 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 . + */ + +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() { + + 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, isEnabled: Boolean) { + appName.text = item.first.label + togglePermission.isChecked = item.second + togglePermission.isEnabled = isEnabled + + itemView.findViewById(R.id.icon).setImageDrawable(item.first.icon) + togglePermission.setOnClickListener { listener(item.first.packageName) } + } + } + + var dataSet: List> = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + var isEnabled: Boolean = true + + fun setData(list: List>, 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 . + */ + +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 . + */ + +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(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 . + */ + +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 . + */ + +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, + fetchMissingIcons: Boolean + ): List { + 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) { + val byUid = mutableMapOf() + val byApId = mutableMapOf() + 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> { + refreshAppDescriptions() + return allProfilesAppDescriptions.map { + it.first.filter { app -> app.profileType == ProfileType.MAIN } + .sortedBy { app -> app.label.toString().lowercase() } + } + } + + fun getMainProfileHiddenSystemApps(): List { + return allProfilesAppDescriptions.value.second.filter { it.profileType == ProfileType.MAIN } + } + + fun apps(): Flow> { + refreshAppDescriptions() + return allProfilesAppDescriptions.map { + it.first.sortedBy { app -> app.label.toString().lowercase() } + } + } + + fun allApps(): Flow> { + return allProfilesAppDescriptions.map { + it.first + it.second + it.third + } + } + + private fun getHiddenSystemApps(): List { + return allProfilesAppDescriptions.value.second + } + + private fun getCompatibilityApps(): List { + 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 mapReduceForHiddenApps( + app: ApplicationDescription, + map: (ApplicationDescription) -> T, + reduce: (List) -> 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() + private var appsByAPId = mapOf() + + 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(), + emptyList(), + emptyList() + ) + ) + + private fun hasInternetPermission(packageInfo: PackageInfo): Boolean { + return packageInfo.requestedPermissions?.contains(Manifest.permission.INTERNET) == true + } + + @Suppress("ReturnCount") + private fun isNotHiddenSystemApp(app: ApplicationInfo, launcherApps: List): 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): 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): 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 . + */ + +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 . + */ + +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 = 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 + 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 = 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 = MutableStateFlow(InternetPrivacyMode.REAL_IP) + + private val _otherVpnRunning = MutableSharedFlow() + suspend fun emitOtherVpnRunning(appDesc: ApplicationDescription) { + _otherVpnRunning.emit(appDesc) + } + val otherVpnRunning: SharedFlow = _otherVpnRunning + + var firstBoot: Boolean + get() = sharedPref.getBoolean(KEY_FIRST_BOOT, true) + set(value) = set(KEY_FIRST_BOOT, value) + + var hideWarningTrackers: Boolean + get() = sharedPref.getBoolean(KEY_HIDE_WARNING_TRACKERS, false) + set(value) = set(KEY_HIDE_WARNING_TRACKERS, value) + + var hideWarningLocation: Boolean + get() = sharedPref.getBoolean(KEY_HIDE_WARNING_LOCATION, false) + set(value) = set(KEY_HIDE_WARNING_LOCATION, value) + + var hideWarningIpScrambling: Boolean + get() = sharedPref.getBoolean(KEY_HIDE_WARNING_IPSCRAMBLING, false) + set(value) = set(KEY_HIDE_WARNING_IPSCRAMBLING, value) + + private fun set(key: String, value: Boolean) { + sharedPref.edit().putBoolean(key, value).apply() + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt new file mode 100644 index 0000000..82915df --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/TrackersRepository.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.data.repositories + +import android.content.Context +import android.util.Log +import com.google.gson.Gson +import foundation.e.privacymodules.trackers.api.Tracker +import retrofit2.Retrofit +import retrofit2.converter.scalars.ScalarsConverterFactory +import retrofit2.http.GET +import java.io.File +import java.io.FileInputStream +import java.io.FileWriter +import java.io.IOException +import java.io.InputStreamReader +import java.io.PrintWriter + +class TrackersRepository(private val context: Context) { + + private val eTrackerFileName = "e_trackers.json" + private val eTrackerFile = File(context.filesDir.absolutePath, eTrackerFileName) + + var trackers: List = emptyList() + private set + + init { + initTrackersFile() + } + + suspend fun update() { + val api = ETrackersApi.build() + saveData(eTrackerFile, api.trackers()) + initTrackersFile() + } + + private fun initTrackersFile() { + try { + var inputStream = context.assets.open(eTrackerFileName) + if (eTrackerFile.exists()) { + inputStream = FileInputStream(eTrackerFile) + } + val reader = InputStreamReader(inputStream, "UTF-8") + val trackerResponse = + Gson().fromJson(reader, ETrackersApi.ETrackersResponse::class.java) + + trackers = mapper(trackerResponse) + + reader.close() + inputStream.close() + } catch (e: Exception) { + Log.e("TrackersRepository", "While parsing trackers in assets", e) + } + } + + private fun mapper(response: ETrackersApi.ETrackersResponse): List { + 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) { + data class ETracker( + val id: String?, + val hostnames: List?, + 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 . + */ + +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 . + */ + +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 . + */ + +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 . + */ + +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 . + */ + +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 . + */ + +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 . + */ + +package foundation.e.advancedprivacy.domain.entities + +data class TrackersPeriodicStatistics( + val callsBlockedNLeaked: List>, + val periods: List, + val trackersCount: Int, + val graduations: List? = 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 . + */ + +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> { + 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 . + */ + +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.REAL_LOCATION, null, null)) + val configuredLocationMode: StateFlow> = _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, 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, 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, + isSpecificLocation: Boolean = false, + ): Triple { + 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(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 . + */ + +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 = 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 = combine( + localStateRepository.blockTrackers, + localStateRepository.areAllTrackersBlocked + ) { isBlockTrackers, isAllTrackersBlocked -> + when { + isBlockTrackers && isAllTrackersBlocked -> TrackerMode.DENIED + isBlockTrackers && !isAllTrackersBlocked -> TrackerMode.CUSTOM + else -> TrackerMode.VULNERABLE + } + } + + val isLocationHidden: Flow = localStateRepository.locationMode.map { locationMode -> + locationMode != LocationMode.REAL_LOCATION + } + + val locationMode: StateFlow = localStateRepository.locationMode + + val ipScramblingMode: Flow = 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 = 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 . + */ + +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 = 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 { + return appListsRepository.getMainProfileHiddenSystemApps().map { it.packageName } + } + + val bypassTorApps: Set 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 . + */ + +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 { + 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 . + */ + +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 . + */ + +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 = 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 { + return TrackersPeriodicStatistics( + callsBlockedNLeaked = trackTrackersPrivacyModule.getPastDayTrackersCalls(), + periods = buildDayLabels(), + trackersCount = trackTrackersPrivacyModule.getPastDayTrackersCount(), + graduations = buildDayGraduations(), + ) to trackTrackersPrivacyModule.getTrackersCount() + } + + fun getNonBlockedTrackersCount(): Flow { + return if (blockTrackersPrivacyModule.isBlockingEnabled()) + appListsRepository.allApps().map { apps -> + val whiteListedTrackers = mutableSetOf() + 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 { + val formatter = DateTimeFormatter.ofPattern( + resources.getString(R.string.trackers_graph_hours_period_format) + ) + + val periods = mutableListOf() + 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 { + val formatter = DateTimeFormatter.ofPattern( + resources.getString(R.string.trackers_graph_hours_period_format) + ) + val periods = mutableListOf() + 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 { + val formater = DateTimeFormatter.ofPattern( + resources.getString(R.string.trackers_graph_days_period_format) + ) + val periods = mutableListOf() + 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 { + val formater = DateTimeFormatter.ofPattern( + resources.getString(R.string.trackers_graph_months_period_format) + ) + val periods = mutableListOf() + 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 { + 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> { + 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 { + return appListsRepository.mapReduceForHiddenApps( + app = app, + map = trackTrackersPrivacyModule::getPastDayTrackersCallsForApp, + reduce = { zip -> + zip.unzip().let { (blocked, leaked) -> + blocked.sum() to leaked.sum() + } + } + ) + } + + fun getAppsWithCounts(): Flow> { + 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 = 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 . + */ + +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 . + */ + +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(R.id.container) + setReorderingAllowed(true) + addToBackStack("dashboard") + } + } + is SingleEvent.NavigateToInternetActivityPrivacySingleEvent -> { + requireActivity().supportFragmentManager.commit { + replace(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(R.id.container) + setReorderingAllowed(true) + addToBackStack("dashboard") + } + } + is SingleEvent.NavigateToAppDetailsEvent -> { + requireActivity().supportFragmentManager.commit { + replace( + 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 . + */ + +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>? = null, + val dayLabels: List? = null, + val dayGraduations: List? = 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 . + */ + +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() + 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 = 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 = 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 . + */ + +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 . + */ + +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 = emptyList(), + val bypassTorApps: Collection = emptyList(), + val selectedLocation: String = "", + val availableLocationIds: List = emptyList(), + val forceRedraw: Boolean = false, +) { + fun getApps(): List> { + 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 . + */ + +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() + 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 = 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..089151e --- /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 . + */ + +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.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.LocationUpdate +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 + } + + override fun onAttach(context: Context) { + super.onAttach(context) + Mapbox.getInstance(requireContext(), getString(R.string.mapbox_key)) + } + + 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(Style.MAPBOX_STREETS) { style -> + enableLocationPlugin(style) + + mapboxMap.addOnCameraMoveListener { + if (binding.mapView.isEnabled) { + mapboxMap.cameraPosition.target.let { + viewModel.submitAction( + Action.SetSpecificLocationAction( + it.latitude.toFloat(), + it.longitude.toFloat() + ) + ) + } + } + } + // 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 + val locationUpdate = LocationUpdate.Builder() + .location(location) + .animationDuration(100) + .build() + locationComponent?.forceLocationUpdate(locationUpdate) + + 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 . + */ + +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 . + */ + +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 . + */ + +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() + val singleEvents = _singleEvents.asSharedFlow() + + private val specificLocationInputFlow = MutableSharedFlow() + + @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 . + */ + +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( + 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 . + */ + +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? = 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 . + */ + +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() + 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 . + */ + +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 . + */ + +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>? = null, + val leaked: Int = 0, + val blocked: Int = 0, + val isTrackersBlockingEnabled: Boolean = false, + val isWhitelistEmpty: Boolean = true, + val showQuickPrivacyDisabledMessage: Boolean = false, +) { + fun getTrackersStatus(): List>? { + 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 . + */ + +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() + 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 . + */ + +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() { + + 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, 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> = emptyList() + + fun updateDataSet(new: List>, 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 . + */ + +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( + containerViewId = R.id.container, + args = intent.extras + ) + ACTION_VIEW_TRACKERS -> { + add(R.id.container) + } + ACTION_VIEW_FAKE_LOCATION -> { + add(R.id.container) + } + ACTION_VIEW_IPSCRAMBLING -> { + add(R.id.container) + } + else -> add(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 . + */ + +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 = 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 { + + 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 . + */ + +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 . + */ + +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> = 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 + } + ) + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt deleted file mode 100644 index aab81d5..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ /dev/null @@ -1,211 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp - -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.privacycentralapp.common.WarningDialog -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.data.repositories.TrackersRepository -import foundation.e.privacycentralapp.domain.usecases.AppListUseCase -import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.domain.usecases.IpScramblingStateUseCase -import foundation.e.privacycentralapp.domain.usecases.ShowFeaturesWarningUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase -import foundation.e.privacycentralapp.dummy.CityDataSource -import foundation.e.privacycentralapp.features.dashboard.DashboardViewModel -import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyViewModel -import foundation.e.privacycentralapp.features.location.FakeLocationViewModel -import foundation.e.privacycentralapp.features.trackers.TrackersViewModel -import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFragment -import foundation.e.privacycentralapp.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 create(modelClass: Class, 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/privacycentralapp/Notifications.kt b/app/src/main/java/foundation/e/privacycentralapp/Notifications.kt deleted file mode 100644 index 0df3e18..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/Notifications.kt +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp - -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.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.MainFeatures -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.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/privacycentralapp/PrivacyCentralApplication.kt b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt deleted file mode 100644 index 2f718b5..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp - -import android.app.Application -import com.mapbox.mapboxsdk.Mapbox -import foundation.e.lib.telemetry.Telemetry - -class PrivacyCentralApplication : Application() { - - // Initialize the dependency container. - val dependencyContainer: DependencyContainer by lazy { DependencyContainer(this) } - - override fun onCreate() { - super.onCreate() - Telemetry.init(BuildConfig.SENTRY_DSN, this, true) - Mapbox.getTelemetry()?.setUserTelemetryRequestState(false) - - dependencyContainer.initBackgroundSingletons() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt b/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt deleted file mode 100644 index 13511da..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp - -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 PrivacyCentralApplication) - .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( - 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/privacycentralapp/common/AppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt deleted file mode 100644 index 2fbbc34..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.R -import foundation.e.privacycentralapp.domain.entities.AppWithCounts - -class AppsAdapter( - private val itemsLayout: Int, - private val listener: (Int) -> Unit -) : - RecyclerView.Adapter() { - - 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 = 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/privacycentralapp/common/BootCompletedReceiver.kt b/app/src/main/java/foundation/e/privacycentralapp/common/BootCompletedReceiver.kt deleted file mode 100644 index d7902ee..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/BootCompletedReceiver.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.common - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import foundation.e.privacycentralapp.Notifications -import foundation.e.privacycentralapp.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/privacycentralapp/common/Factory.kt b/app/src/main/java/foundation/e/privacycentralapp/common/Factory.kt deleted file mode 100644 index 4c7f436..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/Factory.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.common - -// Definition of a Factory interface with a function to create objects of a type -interface Factory { - fun create(): T -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt b/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt deleted file mode 100644 index a25b68e..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt +++ /dev/null @@ -1,333 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.R -import foundation.e.privacycentralapp.common.extensions.dpToPxF -import kotlin.math.floor - -class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbove: Boolean = true) { - var data = emptyList>() - set(value) { - field = value - refreshDataSet() - } - var labels = emptyList() - - var graduations: List? = 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(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(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/privacycentralapp/common/NavToolbarFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/common/NavToolbarFragment.kt deleted file mode 100644 index 6955405..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/NavToolbarFragment.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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/privacycentralapp/common/RightRadioButton.kt b/app/src/main/java/foundation/e/privacycentralapp/common/RightRadioButton.kt deleted file mode 100644 index bbc108b..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/RightRadioButton.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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/privacycentralapp/common/TextViewHelpers.kt b/app/src/main/java/foundation/e/privacycentralapp/common/TextViewHelpers.kt deleted file mode 100644 index d85f4a7..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/TextViewHelpers.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.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/privacycentralapp/common/ThrottleFlow.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt deleted file mode 100644 index 21e1542..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.common - -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlin.time.Duration - -@FlowPreview -fun Flow.throttleFirst(windowDuration: Duration): Flow = 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/privacycentralapp/common/ToggleAppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt deleted file mode 100644 index c41c0cf..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.R -import foundation.e.privacymodules.permissions.data.ApplicationDescription - -class ToggleAppsAdapter( - private val itemsLayout: Int, - private val listener: (String) -> Unit -) : - RecyclerView.Adapter() { - - 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, isEnabled: Boolean) { - appName.text = item.first.label - togglePermission.isChecked = item.second - togglePermission.isEnabled = isEnabled - - itemView.findViewById(R.id.icon).setImageDrawable(item.first.icon) - togglePermission.setOnClickListener { listener(item.first.packageName) } - } - } - - var dataSet: List> = emptyList() - set(value) { - field = value - notifyDataSetChanged() - } - - var isEnabled: Boolean = true - - fun setData(list: List>, 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/privacycentralapp/common/ToolbarFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt deleted file mode 100644 index 5c18548..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.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/privacycentralapp/common/WarningDialog.kt b/app/src/main/java/foundation/e/privacycentralapp/common/WarningDialog.kt deleted file mode 100644 index cbbeffa..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/WarningDialog.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.domain.entities.MainFeatures -import foundation.e.privacycentralapp.domain.entities.MainFeatures.FAKE_LOCATION -import foundation.e.privacycentralapp.domain.entities.MainFeatures.IP_SCRAMBLING -import foundation.e.privacycentralapp.domain.entities.MainFeatures.TRACKERS_CONTROL -import foundation.e.privacycentralapp.domain.usecases.ShowFeaturesWarningUseCase -import foundation.e.privacycentralapp.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(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 PrivacyCentralApplication) - .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/privacycentralapp/common/extensions/AnyExtension.kt b/app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt deleted file mode 100644 index 71de99a..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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/privacycentralapp/data/repositories/AppListsRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt deleted file mode 100644 index a4f7487..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt +++ /dev/null @@ -1,281 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.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, - fetchMissingIcons: Boolean - ): List { - 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) { - val byUid = mutableMapOf() - val byApId = mutableMapOf() - 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> { - refreshAppDescriptions() - return allProfilesAppDescriptions.map { - it.first.filter { app -> app.profileType == ProfileType.MAIN } - .sortedBy { app -> app.label.toString().lowercase() } - } - } - - fun getMainProfileHiddenSystemApps(): List { - return allProfilesAppDescriptions.value.second.filter { it.profileType == ProfileType.MAIN } - } - - fun apps(): Flow> { - refreshAppDescriptions() - return allProfilesAppDescriptions.map { - it.first.sortedBy { app -> app.label.toString().lowercase() } - } - } - - fun allApps(): Flow> { - return allProfilesAppDescriptions.map { - it.first + it.second + it.third - } - } - - private fun getHiddenSystemApps(): List { - return allProfilesAppDescriptions.value.second - } - - private fun getCompatibilityApps(): List { - 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 mapReduceForHiddenApps( - app: ApplicationDescription, - map: (ApplicationDescription) -> T, - reduce: (List) -> 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() - private var appsByAPId = mapOf() - - 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(), - emptyList(), - emptyList() - ) - ) - - private fun hasInternetPermission(packageInfo: PackageInfo): Boolean { - return packageInfo.requestedPermissions?.contains(Manifest.permission.INTERNET) == true - } - - @Suppress("ReturnCount") - private fun isNotHiddenSystemApp(app: ApplicationInfo, launcherApps: List): 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): 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): 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/privacycentralapp/data/repositories/CityDataSource.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/CityDataSource.kt deleted file mode 100644 index d6a6a19..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/CityDataSource.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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/privacycentralapp/data/repositories/LocalStateRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt deleted file mode 100644 index ed97c94..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.data.repositories - -import android.content.Context -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.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 = 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 - 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 = 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 = MutableStateFlow(InternetPrivacyMode.REAL_IP) - - private val _otherVpnRunning = MutableSharedFlow() - suspend fun emitOtherVpnRunning(appDesc: ApplicationDescription) { - _otherVpnRunning.emit(appDesc) - } - val otherVpnRunning: SharedFlow = _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/privacycentralapp/data/repositories/TrackersRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt deleted file mode 100644 index b5310e1..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.data.repositories - -import android.content.Context -import android.util.Log -import com.google.gson.Gson -import foundation.e.privacymodules.trackers.api.Tracker -import retrofit2.Retrofit -import retrofit2.converter.scalars.ScalarsConverterFactory -import retrofit2.http.GET -import java.io.File -import java.io.FileInputStream -import java.io.FileWriter -import java.io.IOException -import java.io.InputStreamReader -import java.io.PrintWriter - -class TrackersRepository(private val context: Context) { - - private val eTrackerFileName = "e_trackers.json" - private val eTrackerFile = File(context.filesDir.absolutePath, eTrackerFileName) - - var trackers: List = emptyList() - private set - - init { - initTrackersFile() - } - - suspend fun update() { - val api = ETrackersApi.build() - saveData(eTrackerFile, api.trackers()) - initTrackersFile() - } - - private fun initTrackersFile() { - try { - var inputStream = context.assets.open(eTrackerFileName) - if (eTrackerFile.exists()) { - inputStream = FileInputStream(eTrackerFile) - } - val reader = InputStreamReader(inputStream, "UTF-8") - val trackerResponse = - Gson().fromJson(reader, ETrackersApi.ETrackersResponse::class.java) - - trackers = mapper(trackerResponse) - - reader.close() - inputStream.close() - } catch (e: Exception) { - Log.e("TrackersRepository", "While parsing trackers in assets", e) - } - } - - private fun mapper(response: ETrackersApi.ETrackersResponse): List { - 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) { - data class ETracker( - val id: String?, - val hostnames: List?, - val name: String?, - val exodusId: String? - ) - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt deleted file mode 100644 index afdd2d5..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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/privacycentralapp/domain/entities/InternetPrivacyMode.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/InternetPrivacyMode.kt deleted file mode 100644 index f849d57..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/InternetPrivacyMode.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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/privacycentralapp/domain/entities/LocationMode.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/LocationMode.kt deleted file mode 100644 index 35a77b3..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/LocationMode.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.entities - -enum class LocationMode { - REAL_LOCATION, RANDOM_LOCATION, SPECIFIC_LOCATION -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/MainFeatures.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/MainFeatures.kt deleted file mode 100644 index 0e7f99c..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/MainFeatures.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.entities - -enum class MainFeatures { - TRACKERS_CONTROL, FAKE_LOCATION, IP_SCRAMBLING -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/QuickPrivacyState.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/QuickPrivacyState.kt deleted file mode 100644 index 3257402..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/QuickPrivacyState.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.entities - -enum class QuickPrivacyState { - DISABLED, ENABLED, FULL_ENABLED; - - fun isEnabled(): Boolean = this != DISABLED -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackerMode.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackerMode.kt deleted file mode 100644 index 9f057be..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackerMode.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.entities - -enum class TrackerMode { - DENIED, CUSTOM, VULNERABLE -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt deleted file mode 100644 index 8ce55dd..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.entities - -data class TrackersPeriodicStatistics( - val callsBlockedNLeaked: List>, - val periods: List, - val trackersCount: Int, - val graduations: List? = null -) diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt deleted file mode 100644 index dd62839..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.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> { - return appListsRepository.mainProfileApps() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt deleted file mode 100644 index 0ff2edb..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.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.REAL_LOCATION, null, null)) - val configuredLocationMode: StateFlow> = _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, 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, 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, - isSpecificLocation: Boolean = false, - ): Triple { - 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(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/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt deleted file mode 100644 index e2c0e7f..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState -import foundation.e.privacycentralapp.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 = 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 = combine( - localStateRepository.blockTrackers, - localStateRepository.areAllTrackersBlocked - ) { isBlockTrackers, isAllTrackersBlocked -> - when { - isBlockTrackers && isAllTrackersBlocked -> TrackerMode.DENIED - isBlockTrackers && !isAllTrackersBlocked -> TrackerMode.CUSTOM - else -> TrackerMode.VULNERABLE - } - } - - val isLocationHidden: Flow = localStateRepository.locationMode.map { locationMode -> - locationMode != LocationMode.REAL_LOCATION - } - - val locationMode: StateFlow = localStateRepository.locationMode - - val ipScramblingMode: Flow = 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 = localStateRepository.otherVpnRunning -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt deleted file mode 100644 index dcb417b..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode.HIDE_IP -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode.HIDE_IP_LOADING -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode.REAL_IP -import foundation.e.privacycentralapp.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 = 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 { - return appListsRepository.getMainProfileHiddenSystemApps().map { it.packageName } - } - - val bypassTorApps: Set 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/privacycentralapp/domain/usecases/ShowFeaturesWarningUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/ShowFeaturesWarningUseCase.kt deleted file mode 100644 index e347b34..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/ShowFeaturesWarningUseCase.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.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 { - 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/privacycentralapp/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt deleted file mode 100644 index afb6d1e..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.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/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt deleted file mode 100644 index 5ca7039..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt +++ /dev/null @@ -1,278 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.usecases - -import android.content.res.Resources -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.throttleFirst -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacycentralapp.domain.entities.AppWithCounts -import foundation.e.privacycentralapp.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 = 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 { - return TrackersPeriodicStatistics( - callsBlockedNLeaked = trackTrackersPrivacyModule.getPastDayTrackersCalls(), - periods = buildDayLabels(), - trackersCount = trackTrackersPrivacyModule.getPastDayTrackersCount(), - graduations = buildDayGraduations(), - ) to trackTrackersPrivacyModule.getTrackersCount() - } - - fun getNonBlockedTrackersCount(): Flow { - return if (blockTrackersPrivacyModule.isBlockingEnabled()) - appListsRepository.allApps().map { apps -> - val whiteListedTrackers = mutableSetOf() - 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 { - val formatter = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_hours_period_format) - ) - - val periods = mutableListOf() - 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 { - val formatter = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_hours_period_format) - ) - val periods = mutableListOf() - 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 { - val formater = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_days_period_format) - ) - val periods = mutableListOf() - 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 { - val formater = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_months_period_format) - ) - val periods = mutableListOf() - 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 { - 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> { - 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 { - return appListsRepository.mapReduceForHiddenApps( - app = app, - map = trackTrackersPrivacyModule::getPastDayTrackersCallsForApp, - reduce = { zip -> - zip.unzip().let { (blocked, leaked) -> - blocked.sum() to leaked.sum() - } - } - ) - } - - fun getAppsWithCounts(): Flow> { - 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 = 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/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt deleted file mode 100644 index f70065c..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.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/privacycentralapp/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt deleted file mode 100644 index 0dc24e8..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt +++ /dev/null @@ -1,307 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.GraphHolder -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.databinding.FragmentDashboardBinding -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState -import foundation.e.privacycentralapp.domain.entities.TrackerMode -import foundation.e.privacycentralapp.features.dashboard.DashboardViewModel.Action -import foundation.e.privacycentralapp.features.dashboard.DashboardViewModel.SingleEvent -import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyFragment -import foundation.e.privacycentralapp.features.location.FakeLocationFragment -import foundation.e.privacycentralapp.features.trackers.TrackersFragment -import foundation.e.privacycentralapp.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 PrivacyCentralApplication).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(R.id.container) - setReorderingAllowed(true) - addToBackStack("dashboard") - } - } - is SingleEvent.NavigateToInternetActivityPrivacySingleEvent -> { - requireActivity().supportFragmentManager.commit { - replace(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(R.id.container) - setReorderingAllowed(true) - addToBackStack("dashboard") - } - } - is SingleEvent.NavigateToAppDetailsEvent -> { - requireActivity().supportFragmentManager.commit { - replace( - 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/privacycentralapp/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt deleted file mode 100644 index 0e3521d..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.features.dashboard - -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState -import foundation.e.privacycentralapp.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>? = null, - val dayLabels: List? = null, - val dayGraduations: List? = null, -) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt deleted file mode 100644 index f3a9774..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* -* 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 . - */ - -package foundation.e.privacycentralapp.features.dashboard - -import androidx.annotation.StringRes -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.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() - 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 = 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 = 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/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt deleted file mode 100644 index afef986..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt +++ /dev/null @@ -1,201 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.common.ToggleAppsAdapter -import foundation.e.privacycentralapp.common.setToolTipForAsterisk -import foundation.e.privacycentralapp.databinding.FragmentInternetActivityPolicyBinding -import foundation.e.privacycentralapp.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 PrivacyCentralApplication).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/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt deleted file mode 100644 index 54b7e01..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.features.internetprivacy - -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacymodules.permissions.data.ApplicationDescription - -data class InternetPrivacyState( - val mode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP, - val availableApps: List = emptyList(), - val bypassTorApps: Collection = emptyList(), - val selectedLocation: String = "", - val availableLocationIds: List = emptyList(), - val forceRedraw: Boolean = false, -) { - fun getApps(): List> { - return availableApps.map { it to (it.packageName !in bypassTorApps) } - } - - val selectedLocationPosition get() = availableLocationIds.indexOf(selectedLocation) -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt deleted file mode 100644 index bbd6239..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.features.internetprivacy - -import androidx.annotation.StringRes -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.usecases.AppListUseCase -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.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() - 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 = 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/privacycentralapp/features/location/FakeLocationFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt deleted file mode 100644 index 9e3f854..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt +++ /dev/null @@ -1,376 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.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.LocationUpdate -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.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.databinding.FragmentFakeLocationBinding -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.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 PrivacyCentralApplication).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 - } - - override fun onAttach(context: Context) { - super.onAttach(context) - Mapbox.getInstance(requireContext(), getString(R.string.mapbox_key)) - } - - 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(Style.MAPBOX_STREETS) { style -> - enableLocationPlugin(style) - - mapboxMap.addOnCameraMoveListener { - if (binding.mapView.isEnabled) { - mapboxMap.cameraPosition.target.let { - viewModel.submitAction( - Action.SetSpecificLocationAction( - it.latitude.toFloat(), - it.longitude.toFloat() - ) - ) - } - } - } - // 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 - val locationUpdate = LocationUpdate.Builder() - .location(location) - .animationDuration(100) - .build() - locationComponent?.forceLocationUpdate(locationUpdate) - - 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/privacycentralapp/features/location/FakeLocationMapView.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt deleted file mode 100644 index e71bfcc..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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/privacycentralapp/features/location/FakeLocationState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt deleted file mode 100644 index 50d7a14..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.features.location - -import android.location.Location -import foundation.e.privacycentralapp.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/privacycentralapp/features/location/FakeLocationViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt deleted file mode 100644 index 1cdf9f4..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.features.location - -import android.location.Location -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase -import foundation.e.privacycentralapp.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() - val singleEvents = _singleEvents.asSharedFlow() - - private val specificLocationInputFlow = MutableSharedFlow() - - @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/privacycentralapp/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt deleted file mode 100644 index cb32c2c..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt +++ /dev/null @@ -1,218 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.AppsAdapter -import foundation.e.privacycentralapp.common.GraphHolder -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.common.setToolTipForAsterisk -import foundation.e.privacycentralapp.databinding.FragmentTrackersBinding -import foundation.e.privacycentralapp.databinding.TrackersItemGraphBinding -import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics -import foundation.e.privacycentralapp.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 PrivacyCentralApplication).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( - 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/privacycentralapp/features/trackers/TrackersState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt deleted file mode 100644 index a3bb80a..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.features.trackers - -import foundation.e.privacycentralapp.domain.entities.AppWithCounts -import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics - -data class TrackersState( - val dayStatistics: TrackersPeriodicStatistics? = null, - val monthStatistics: TrackersPeriodicStatistics? = null, - val yearStatistics: TrackersPeriodicStatistics? = null, - val apps: List? = null, -) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt deleted file mode 100644 index 8b5cc32..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.features.trackers - -import android.net.Uri -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.domain.entities.AppWithCounts -import foundation.e.privacycentralapp.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() - 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/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt deleted file mode 100644 index 888c140..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.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 PrivacyCentralApplication).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/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt deleted file mode 100644 index a190a74..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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>? = null, - val leaked: Int = 0, - val blocked: Int = 0, - val isTrackersBlockingEnabled: Boolean = false, - val isWhitelistEmpty: Boolean = true, - val showQuickPrivacyDisabledMessage: Boolean = false, -) { - fun getTrackersStatus(): List>? { - 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/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt deleted file mode 100644 index e5a94f9..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.features.trackers.apptrackers - -import android.net.Uri -import androidx.annotation.StringRes -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.domain.entities.TrackerMode -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase -import foundation.e.privacycentralapp.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() - 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/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt deleted file mode 100644 index 197f13f..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.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() { - - 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, 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> = emptyList() - - fun updateDataSet(new: List>, 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/privacycentralapp/main/MainActivity.kt b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt deleted file mode 100644 index 92dc326..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.R -import foundation.e.privacycentralapp.features.dashboard.DashboardFragment -import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyFragment -import foundation.e.privacycentralapp.features.location.FakeLocationFragment -import foundation.e.privacycentralapp.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( - containerViewId = R.id.container, - args = intent.extras - ) - ACTION_VIEW_TRACKERS -> { - add(R.id.container) - } - ACTION_VIEW_FAKE_LOCATION -> { - add(R.id.container) - } - ACTION_VIEW_IPSCRAMBLING -> { - add(R.id.container) - } - else -> add(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/privacycentralapp/widget/Widget.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt deleted file mode 100644 index 3abe21b..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp - -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.content.Context -import android.os.Bundle -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase -import foundation.e.privacycentralapp.widget.State -import foundation.e.privacycentralapp.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 = 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 { - - 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/privacycentralapp/widget/WidgetCommandReceiver.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt deleted file mode 100644 index e01f47f..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.widget - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import foundation.e.privacycentralapp.PrivacyCentralApplication - -class WidgetCommandReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val getQuickPrivacyStateUseCase = (context?.applicationContext as? PrivacyCentralApplication)?.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/privacycentralapp/widget/WidgetUI.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt deleted file mode 100644 index fccfd48..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt +++ /dev/null @@ -1,381 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.privacycentralapp.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.privacycentralapp.R -import foundation.e.privacycentralapp.Widget -import foundation.e.privacycentralapp.Widget.Companion.isDarkText -import foundation.e.privacycentralapp.common.extensions.dpToPxF -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState -import foundation.e.privacycentralapp.domain.entities.TrackerMode -import foundation.e.privacycentralapp.main.MainActivity -import foundation.e.privacycentralapp.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_IPSCRAMBLING -import foundation.e.privacycentralapp.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_LOCATION -import foundation.e.privacycentralapp.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> = 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 - } - ) - } -} diff --git a/app/src/main/res/layout/fragment_fake_location.xml b/app/src/main/res/layout/fragment_fake_location.xml index 47b86bf..0c95fc8 100644 --- a/app/src/main/res/layout/fragment_fake_location.xml +++ b/app/src/main/res/layout/fragment_fake_location.xml @@ -41,7 +41,7 @@ android:layout_marginTop="16dp" > - - - - - - -