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 --- .idea/.name | 2 +- DEVELOPMENT.md | 10 +- README.md | 19 +- 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 -- build.gradle | 4 +- .../advancedprivacy/buildsrc/DependencyUpdates.kt | 42 +++ .../e/privacycentral/buildsrc/DependencyUpdates.kt | 42 --- .../fakelocationdemo/src/main/AndroidManifest.xml | 2 +- .../src/main/res/values/themes.xml | 2 +- ...pp-permissions-foundation.e.advancedprivacy.xml | 10 + ...-permissions-foundation.e.privacycentralapp.xml | 10 - scripts/sign_and_push.sh | 17 - settings.gradle | 2 +- 130 files changed, 6306 insertions(+), 6312 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 create mode 100644 buildSrc/src/main/java/foundation/e/advancedprivacy/buildsrc/DependencyUpdates.kt delete mode 100644 buildSrc/src/main/java/foundation/e/privacycentral/buildsrc/DependencyUpdates.kt create mode 100644 privapp-permissions-foundation.e.advancedprivacy.xml delete mode 100644 privapp-permissions-foundation.e.privacycentralapp.xml delete mode 100755 scripts/sign_and_push.sh diff --git a/.idea/.name b/.idea/.name index d32d4d9..5f54997 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -PrivacyCentralApp \ No newline at end of file +AdvancedPrivacy diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2743aac..88354c6 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,11 +1,11 @@ -# PrivacyCentral Development Guide +# AdvancedPrivacy Development Guide This guide contains development related information to help a developer in getting better understanding of project structure. ## Architecture -The architecture of PrivacyCentralApp is based on [clean architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html). For presentation layer, we use [Model-View-Intent](http://hannesdorfmann.com/android/model-view-intent/) design pattern which is a unidirectional reactive flow pattern. We use it in conjunction to ViewModel to make our features lifecycle aware. Our android app is having single activity multiple fragments. +The architecture of AdvancedPrivacy is based on [clean architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html). For presentation layer, we use [Model-View-Intent](http://hannesdorfmann.com/android/model-view-intent/) design pattern which is a unidirectional reactive flow pattern. We use it in conjunction to ViewModel to make our features lifecycle aware. Our android app is having single activity multiple fragments. ### Clean Architecture -Clean architecture is the building block of PrivacyCentralApp. This architecture is based on the following principles: +Clean architecture is the building block of AdvancedPrivacy. This architecture is based on the following principles: 1. **Independent of Frameworks**. The architecture does not depend on the existence of some library of feature laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints. 2. **Testable**. The business rules can be tested without the UI, Database, Web Server, or any other external element. 3. **Independent of UI**. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules. @@ -19,7 +19,7 @@ Clean architecture is the building block of PrivacyCentralApp. This architecture MVI is used at the presentation layer of clean architecture. It is very much similar to [Redux](https://redux.js.org/) in terms of implementation and working. It has three main components. -- **View**: This is where activities, fragments and other android components live. It is responsible for publishing user intent/actions to the model and rendering the state returned by the model. In PrivacyCentralApp, it is just an interface which is implemented by android components. +- **View**: This is where activities, fragments and other android components live. It is responsible for publishing user intent/actions to the model and rendering the state returned by the model. In AdvancedPrivacy, it is just an interface which is implemented by android components. - **Intent**: In context of our app, we call them actions. These are simple data classes having any extra payload like inputs, ids etc. - **Model (data layer at presentation level)**: This is responsible for processing the actions, communicating with domain use-cases and mutating the state of the model. It acts as a Store from redux but for our use case, we call it a **Feature**. @@ -33,7 +33,7 @@ Elements of a feature: 2. **State**: Simple POJO (kotlin data class) representing various UI states of the application. 3. **SingleEventProducer**: This is a function which is invoked by the reducer to publish single events (that can/should only be consumed once like displaying toast, snackbar message or sending an analytics event). This function takes action, effect, current state as input and it returns a `SingleEvent`. By default this function is null for any Feature. -### Architecture Overview of PrivacyCentral App +### Architecture Overview of AdvancedPrivacy App ![](art/privacycentral-arch.png) diff --git a/README.md b/README.md index 9d58239..35a7958 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,6 @@ modules must be build and deployed in the following order: - ./gradlew :privacymodule-api:assembleRelease - ./gradlew --console=verbose publishToMavenLocal // To make it available locally for dev. - ./gradlew --console=verbose publish // To publish it on gitlab for release. -- [privacymodule-e](../../../e_privacycentral_privacymodulese) - [privacymodule-tor](../../../e_privacycentral_privacymoduletor) - [app] @@ -117,6 +116,18 @@ For building from CLI, you can execute this command: You can build the apk locally by using above instructions or you can download the latest stable apk from `master` branch pipeline. ### To run apk on /e/OS devices + +If you are running your tests on a `/test` build, the debug buildtype already sign it with the appropriate key, and without the persistant flag, to allow further updates. +But the first time, to replace the AdvancedPrivacy app, embeded in the test build, you have to use the following commands: + + ```shell + adb root && adb remount + adb push your_advanced_privacy_debug_build.apk /system/priv-app/AdvancedPrivacy/AdvancedPrivacy.apk + adb shell kill -9 $(adb shell pidof -s foundation.e.advancedprivacy) + ``` + +#### AdvancedPrivacy requiement against the system + AdvancedPrivacy needs to be installed as system app and whitelisting in order to grant some system specific permissions. Follow these steps to make it work properly on /e/OS 1. From `Developer options`, enable `Android debugging` and `Rooted debugging` @@ -135,13 +146,13 @@ AdvancedPrivacy needs to be installed as system app and whitelisting in order to ``` 1. Push permissions whitelist. - - it requires the whitelisting [privapp-permissions-foundation.e.privacycentralapp.xml](privapp-permissions-foundation.e.privacycentralapp.xml) file that can be found in the project repository. + - it requires the whitelisting [privapp-permissions-foundation.e.advancedprivacy.xml](privapp-permissions-foundation.e.advancedprivacy.xml) file that can be found in the project repository. - then use the following command ```bash - adb push privapp-permissions-foundation.e.privacycentralapp.xml /system/etc/permissions/ + adb push privapp-permissions-foundation.e.advancedprivacy.xml /system/etc/permissions/ ``` -1. Allow the fake location service to run in background. Add in the file /system/etc/permissions/platform.xml . +1. Allow the fake location service to run in background. Add in the file /system/etc/permissions/platform.xml . 1. Reboot the device ```shell 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" > - - - - - - -