summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2022-07-28 06:04:25 +0000
committerGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2022-07-28 06:04:25 +0000
commit12510a55c9c2b1d21c6e1f45d0058778ddfc9eaa (patch)
treef87e29f670323b7173e5e3875112271c8835a5d3
parent3ca73e64ddd25c7c20eca2e4e0db77032db848c0 (diff)
parentb4d35c1c12120503e74d7ae99edd94302673acf6 (diff)
Merge branch 'remove_flow_mvi' into 'main'
#5444 Fix CPU consumption - remove flow-mvi dependency See merge request e/os/advanced-privacy!74
-rw-r--r--.gitignore3
-rw-r--r--DEVELOPMENT.md184
-rw-r--r--app/build.gradle38
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt104
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt (renamed from flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt)23
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt (renamed from app/src/main/java/foundation/e/privacycentralapp/extensions/AnyExtension.kt)2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt6
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt16
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt3
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt4
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt14
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt233
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt168
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt35
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt128
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt243
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt93
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt37
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt131
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt153
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt70
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt (renamed from app/src/main/java/foundation/e/privacycentralapp/extensions/ViewModelExtension.kt)22
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt99
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt158
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt103
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt (renamed from app/src/main/java/foundation/e/privacycentralapp/main/MainViewModel.kt)15
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt75
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt242
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt104
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt45
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt116
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt1
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt8
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt6
-rw-r--r--app/src/main/res/layout/fragment_trackers.xml137
-rw-r--r--app/src/main/res/values-es/strings.xml2
-rw-r--r--build.gradle16
-rw-r--r--dependencies.gradle7
-rw-r--r--flow-mvi/.gitignore1
-rw-r--r--flow-mvi/build.gradle31
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt24
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt42
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt130
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt62
-rw-r--r--gradle.properties2
-rw-r--r--settings.gradle1
49 files changed, 1089 insertions, 2054 deletions
diff --git a/.gitignore b/.gitignore
index 7ece0fd..9572f6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,9 @@ local.properties
/.idea/jarRepositories.xml
/.idea/google-java-format.xml
/.idea/runConfigurations.xml
+/.idea/dbnavigator.xml
+/.idea/deploymentTargetDropDown.xml
+
gradle.xml
markdown-*.xml
*.iml
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 75e1535..2743aac 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -29,11 +29,9 @@ In this app, we have implemented MVI using [Kotlin Flow](https://kotlinlang.org/
<img src="art/MVI-Feature.png" width="336" height="332">
Elements of a feature:
-1. **Actor**: It is just a function that takes current state, user action as input and produces an effect (result) as output. This function generally makes the call to external APIs and usecases.
-2. **Reducer**: It is also a very simple function whose inputs are current state, effect from the actor and it returns new state.
-3. **State**: Simple POJO (kotlin data class) representing various UI states of the application.
-4. **Effect**: A POJO (kotlin data class) which is returned from the actor function.
-5. **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.
+1. **Action**: The exhaustive list of user actions for 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
@@ -50,179 +48,6 @@ Looking at the diagram from right to left:
8. **ViewModel**: arch-component lifecycle aware viewmodel.
9. **Views**: Android high level components like activities, fragments, etc.
-## How to implement a new feature
-Imaging you have to implement a fake location feature.
-1. Create a new package under `features` called `fakelocation`
-2. Create a new feature class called `FakeLocationFeature` and make it extend the BaseFeature class as below:
-```kotlin
-class FakeLocationFeature(
- initialState: State,
- coroutineScope: CoroutineScope,
- reducer: Reducer<State, Effect>,
- actor: Actor<State, Action, Effect>,
- singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
-) : BaseFeature<FakeLocationFeature.State, FakeLocationFeature.Action, FakeLocationFeature.Effect, FakeLocationFeature.SingleEvent>(
- initialState,
- actor,
- reducer,
- coroutineScope,
- { message -> Log.d("FakeLocationFeature", message) },
- singleEventProducer
-) {
- // Other elements goes here.
-}
-```
-
-3. Define various elements for the feature in the above class
-```kotlin
-// State to be reflected in the UI
-data class State(val location: Location)
-
-// User triggered actions
-sealed class Action {
- data class UpdateLocationAction(val latLng: LatLng) : Action()
- object UseRealLocationAction : Action()
- object UseSpecificLocationAction : Action()
- data class SetFakeLocationAction(val latitude: Double, val longitude: Double) : Action()
-}
-
-// Output from the actor after processing an action
-sealed class Effect {
- data class LocationUpdatedEffect(val latitude: Double, val longitude: Double) : Effect()
- object RealLocationSelectedEffect : Effect()
- ...
- ...
- data class ErrorEffect(val message: String) : Effect()
-}
-```
-
-4. Create a static `create` function in feature which returns the feature instance:
-```kotlin
-companion object {
- fun create(
- initialState: State = <initial state>
- coroutineScope: CoroutineScope
- ) = FakeLocationFeature(
- initialState, coroutineScope,
- reducer = { state, effect ->
- when (effect) {
- Effect.RealLocationSelectedEffect -> state.copy(
- location = state.location.copy(
- mode = LocationMode.REAL_LOCATION
- )
- )
- is Effect.ErrorEffect, Effect.SpecificLocationSavedEffect -> state
- is Effect.LocationUpdatedEffect -> state.copy(
- location = state.location.copy(
- latitude = effect.latitude,
- longitude = effect.longitude
- )
- )
- }
- },
- actor = { _, action ->
- when (action) {
- is Action.UpdateLocationAction -> flowOf(
- Effect.LocationUpdatedEffect(
- action.latLng.latitude,
- action.latLng.longitude
- )
- )
- is Action.SetFakeLocationAction -> {
- val location = Location(
- LocationMode.CUSTOM_LOCATION,
- action.latitude,
- action.longitude
- )
- // TODO: Call fake location api with specific coordinates here.
- val success = DummyDataSource.setLocationMode(
- LocationMode.CUSTOM_LOCATION,
- location
- )
- if (success) {
- flowOf(
- Effect.SpecificLocationSavedEffect
- )
- } else {
- flowOf(
- Effect.ErrorEffect("Couldn't select location")
- )
- }
- }
- Action.UseRealLocationAction -> {
- // TODO: Call turn off fake location api here.
- val success = DummyDataSource.setLocationMode(LocationMode.REAL_LOCATION)
- if (success) {
- flowOf(
- Effect.RealLocationSelectedEffect
- )
- } else {
- flowOf(
- Effect.ErrorEffect("Couldn't select location")
- )
- }
- }
- Action.UseSpecificLocationAction -> {
- flowOf(Effect.SpecificLocationSelectedEffect)
- }
- }
- },
- singleEventProducer = { _, _, effect ->
- when (effect) {
- Effect.SpecificLocationSavedEffect -> SingleEvent.SpecificLocationSavedEvent
- Effect.RealLocationSelectedEffect -> SingleEvent.RealLocationSelectedEvent
- is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
- else -> null
- }
- }
- )
- }
-```
-
-5. Create a `viewmodel` like below:
-```kotlin
-class FakeLocationViewModel : ViewModel() {
-
- private val _actions = MutableSharedFlow<FakeLocationFeature.Action>()
- val actions = _actions.asSharedFlow()
-
- val fakeLocationFeature: FakeLocationFeature by lazy {
- FakeLocationFeature.create(coroutineScope = viewModelScope)
- }
-
- fun submitAction(action: FakeLocationFeature.Action) {
- viewModelScope.launch {
- _actions.emit(action)
- }
- }
-}
-```
-
-6. Create a `fragment` for your feature and make sure it implements `MVIView<>` interface
-7. Initialize (or retrieve the existing) instance of viewmodel in your `fragment` class by using extension function.
-```kotlin
-private val viewModel: FakeLocationViewModel by viewModels()
-```
-
-8. In `onCreate` method of fragment, launch a coroutine to bind the view to feature and to listen single events.
-```kotlin
-override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- lifecycleScope.launchWhenStarted {
- viewModel.fakeLocationFeature.takeView(this, this@FakeLocationFragment)
- }
- lifecycleScope.launchWhenStarted {
- viewModel.fakeLocationFeature.singleEvents.collect { event ->
- // Do something with event
- }
- }
-}
-```
-
-9. To render the state in UI, override the `render` function of MVIView.
-10. For publishing ui actions, use `viewModel.submitAction(action)`.
-
-Everything is lifecycle aware so we don't need to anything manually here.
## Code Quality and Style
This project integrates a combination of unit tests, functional test and code styling tools.
To run **unit** tests on your machine:
@@ -240,13 +65,10 @@ To run code style check and formatting tool:
The project currently doesn't have exactly the same mentioned structure as it is just a POC and will be improved.
### Todo/Improvements
-- [ ] Add domain layer with usecases.
-- [ ] Add data layer with repository implementation.
- [ ] Add unit tests and code coverage.
- [ ] Implement Hilt DI.
# References
1. [Kotlin Flow](https://kotlinlang.org/docs/flow.html)
2. [MVI](https://hannesdorfmann.com/android/mosby3-mvi-1/)
-3. [Redux](https://redux.js.org/)
4. [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) \ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 23d6ecd..5f2b302 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -45,7 +45,7 @@ android {
productFlavors {
e29 {
dimension 'os'
- minSdkVersion 26
+ minSdkVersion 29
targetSdkVersion 29
}
e30 {
@@ -103,7 +103,7 @@ android {
}
dependencies {
- implementation 'androidx.work:work-runtime-ktx:2.5.0'
+
compileOnly files('libs/e-ui-sdk-1.0.1-q.jar')
implementation files('libs/lineage-sdk.jar')
// include the google specific version of the modules, just for the google flavor
@@ -116,21 +116,35 @@ dependencies {
e30Implementation 'foundation.e:privacymodule.e-30:0.4.3'
implementation 'foundation.e:privacymodule.tor:0.2.4'
- implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
+
+
+ // implementation Libs.Kotlin.stdlib
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$Versions.kotlin"
+// implementation Libs.AndroidX.coreKtx
+ implementation "androidx.core:core-ktx:1.8.0"
+
+// implementation Libs.AndroidX.Fragment.fragmentKtx
+ implementation "androidx.fragment:fragment-ktx:$Versions.fragment"
+
+ implementation 'androidx.appcompat:appcompat:1.4.2'
+// implementation Libs.AndroidX.Lifecycle.runtime
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$Versions.lifecycle"
+// implementation Libs.AndroidX.Lifecycle.viewmodel
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$Versions.lifecycle"
+
+ implementation 'androidx.work:work-runtime-ktx:2.7.1'
+
+ implementation 'com.google.android.material:material:1.6.1'
+
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
- implementation project(":flow-mvi")
- implementation Libs.Kotlin.stdlib
- implementation Libs.AndroidX.coreKtx
- implementation Libs.AndroidX.Fragment.fragmentKtx
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation Libs.AndroidX.Lifecycle.runtime
- implementation Libs.AndroidX.Lifecycle.viewmodel
+// implementation Libs.MapBox.sdk
+ implementation "com.mapbox.mapboxsdk:mapbox-android-sdk:$Versions.mapbox"
+ implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
+
- implementation Libs.MapBox.sdk
- implementation 'com.google.android.material:material:1.4.0-beta01'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
index 727d00d..6be3724 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
@@ -20,6 +20,10 @@ 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.data.repositories.AppListsRepository
import foundation.e.privacycentralapp.data.repositories.LocalStateRepository
import foundation.e.privacycentralapp.data.repositories.TrackersRepository
@@ -30,11 +34,12 @@ import foundation.e.privacycentralapp.domain.usecases.IpScramblingStateUseCase
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.DashBoardViewModelFactory
-import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyViewModelFactory
-import foundation.e.privacycentralapp.features.location.FakeLocationViewModelFactory
-import foundation.e.privacycentralapp.features.trackers.TrackersViewModelFactory
-import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersViewModelFactory
+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.ipscrambler.IpScramblerModule
import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
import foundation.e.privacymodules.location.FakeLocationModule
@@ -43,14 +48,15 @@ import foundation.e.privacymodules.permissions.PermissionsPrivacyModule
import foundation.e.privacymodules.permissions.data.ApplicationDescription
import foundation.e.privacymodules.trackers.api.BlockTrackersPrivacyModule
import foundation.e.privacymodules.trackers.api.TrackTrackersPrivacyModule
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.GlobalScope
/**
* Simple container to hold application wide dependencies.
*
- * TODO: Test if this implementation is leaky.
*/
+@OptIn(DelicateCoroutinesApi::class)
class DependencyContainer(val app: Application) {
val context: Context by lazy { app.applicationContext }
@@ -102,32 +108,17 @@ class DependencyContainer(val app: Application) {
)
}
- // ViewModelFactories
- val dashBoardViewModelFactory by lazy {
- DashBoardViewModelFactory(getQuickPrivacyStateUseCase, trackersStatisticsUseCase)
- }
-
- val fakeLocationViewModelFactory by lazy {
- FakeLocationViewModelFactory(
- getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
- fakeLocationStateUseCase = fakeLocationStateUseCase
- )
- }
-
- val internetPrivacyViewModelFactory by lazy {
- InternetPrivacyViewModelFactory(ipScramblerModule, getQuickPrivacyStateUseCase, ipScramblingStateUseCase, appListUseCase)
- }
-
- val trackersViewModelFactory by lazy {
- TrackersViewModelFactory(getQuickPrivacyStateUseCase, trackersStatisticsUseCase)
- }
-
- val appTrackersViewModelFactory by lazy {
- AppTrackersViewModelFactory(trackersStateUseCase, trackersStatisticsUseCase, getQuickPrivacyStateUseCase)
- }
+ val viewModelsFactory by lazy { ViewModelsFactory(
+ getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
+ trackersStatisticsUseCase = trackersStatisticsUseCase,
+ trackersStateUseCase = trackersStateUseCase,
+ fakeLocationStateUseCase = fakeLocationStateUseCase,
+ ipScramblerModule = ipScramblerModule,
+ ipScramblingStateUseCase = ipScramblingStateUseCase,
+ appListUseCase = appListUseCase
+ ) }
// Background
- @FlowPreview
fun initBackgroundSingletons() {
trackersStateUseCase
ipScramblingStateUseCase
@@ -142,3 +133,56 @@ class DependencyContainer(val app: Application) {
)
}
}
+
+class ViewModelsFactory(
+ private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
+ private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
+ private val trackersStateUseCase: TrackersStateUseCase,
+ private val fakeLocationStateUseCase: FakeLocationStateUseCase,
+ private val ipScramblerModule: IIpScramblerModule,
+ private val ipScramblingStateUseCase: IpScramblingStateUseCase,
+ private val appListUseCase: AppListUseCase
+): ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
+ return when (modelClass) {
+ AppTrackersViewModel::class.java -> {
+ val fallbackUid = android.os.Process.myPid()
+ val appUid = extras[DEFAULT_ARGS_KEY]?.
+ getInt(AppTrackersFragment.PARAM_APP_UID, fallbackUid)?: fallbackUid
+
+ AppTrackersViewModel(
+ appUid = appUid,
+ trackersStateUseCase = trackersStateUseCase,
+ trackersStatisticsUseCase = trackersStatisticsUseCase,
+ getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase
+ )
+ }
+
+ TrackersViewModel::class.java ->
+ TrackersViewModel(
+ getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
+ 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/PrivacyCentralApplication.kt b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt
index 2d90c93..b23be3d 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt
@@ -26,7 +26,7 @@ class PrivacyCentralApplication : Application() {
// Initialize the dependency container.
val dependencyContainer: DependencyContainer by lazy { DependencyContainer(this) }
- @FlowPreview
+
override fun onCreate() {
super.onCreate()
Mapbox.getTelemetry()?.setUserTelemetryRequestState(false)
diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt b/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt
index 32766ca..d7a9dd0 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt
@@ -40,7 +40,7 @@ import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import com.github.mikephil.charting.utils.MPPointF
import foundation.e.privacycentralapp.R
-import foundation.e.privacycentralapp.extensions.dpToPxF
+import foundation.e.privacycentralapp.common.extensions.dpToPxF
class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbove: Boolean = true) {
var data = emptyList<Pair<Int, Int>>()
diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt
index aa6f624..21e1542 100644
--- a/flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 E FOUNDATION
+ * 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
@@ -15,13 +15,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package foundation.e.flowmvi
+package foundation.e.privacycentralapp.common
+import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlin.time.Duration
-interface MVIView<State, out Action> {
-
- fun render(state: State)
-
- fun actions(): Flow<Action>
+@FlowPreview
+fun <T> Flow<T>.throttleFirst(windowDuration: Duration): Flow<T> = flow {
+ var lastEmissionTime = 0L
+ collect { upstream ->
+ val currentTime = System.currentTimeMillis()
+ val mayEmit = currentTime - lastEmissionTime > windowDuration.inWholeMilliseconds
+ if (mayEmit) {
+ lastEmissionTime = currentTime
+ emit(upstream)
+ }
+ }
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/extensions/AnyExtension.kt b/app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt
index 2074b69..5c73df9 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/extensions/AnyExtension.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package foundation.e.privacycentralapp.extensions
+package foundation.e.privacycentralapp.common.extensions
import android.content.Context
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
index b4bca0b..af8646a 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt
@@ -48,7 +48,7 @@ class LocalStateRepository(context: Context) {
return isFirstActivation
}
- var quickPrivacyEnabledFlow: Flow<Boolean> = quickPrivacyEnabledMutableFlow
+ var quickPrivacyEnabledFlow: StateFlow<Boolean> = quickPrivacyEnabledMutableFlow
val areAllTrackersBlocked: MutableStateFlow<Boolean> = MutableStateFlow(false)
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
index 5446d3b..aa4276d 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt
@@ -35,7 +35,6 @@ 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.collect
import kotlinx.coroutines.launch
import kotlin.random.Random
@@ -144,11 +143,12 @@ class FakeLocationStateUseCase(
// Deprecated since API 29, never called.
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
- override fun onProviderEnabled(provider: String?) {
+ // TODO migration to minSdk31 , check still working.
+ override fun onProviderEnabled(provider: String) {
reset(provider)
}
- override fun onProviderDisabled(provider: String?) {
+ override fun onProviderDisabled(provider: String) {
reset(provider)
}
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
index 36599cb..7377568 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt
@@ -17,11 +17,13 @@
package foundation.e.privacycentralapp.domain.usecases
+import android.util.Log
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 kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
@@ -29,7 +31,7 @@ import kotlinx.coroutines.launch
class GetQuickPrivacyStateUseCase(
private val localStateRepository: LocalStateRepository,
- private val coroutineScope: CoroutineScope
+ coroutineScope: CoroutineScope
) {
init {
@@ -40,9 +42,11 @@ class GetQuickPrivacyStateUseCase(
}
}
- val quickPrivacyEnabledFlow = localStateRepository.quickPrivacyEnabledFlow
+ val quickPrivacyEnabledFlow: Flow<Boolean> = localStateRepository.quickPrivacyEnabledFlow
- val quickPrivacyState = combine(
+ val isQuickPrivacyEnabled: Boolean get() = localStateRepository.isQuickPrivacyEnabled
+
+ val quickPrivacyState: Flow<QuickPrivacyState> = combine(
localStateRepository.quickPrivacyEnabledFlow,
localStateRepository.areAllTrackersBlocked,
localStateRepository.locationMode,
@@ -60,14 +64,14 @@ class GetQuickPrivacyStateUseCase(
}
}
- val isTrackersDenied = combine(
+ val isTrackersDenied: Flow<Boolean> = combine(
localStateRepository.quickPrivacyEnabledFlow,
localStateRepository.areAllTrackersBlocked
) { isQuickPrivacyEnabled, isAllTrackersBlocked ->
isQuickPrivacyEnabled && isAllTrackersBlocked
}
- val isLocationHidden = combine(
+ val isLocationHidden: Flow<Boolean> = combine(
localStateRepository.quickPrivacyEnabledFlow,
localStateRepository.locationMode
) { isQuickPrivacyEnabled, locationMode ->
@@ -76,7 +80,7 @@ class GetQuickPrivacyStateUseCase(
val locationMode: StateFlow<LocationMode> = localStateRepository.locationMode
- val isIpHidden = combine(
+ val isIpHidden: Flow<Boolean?> = combine(
localStateRepository.quickPrivacyEnabledFlow,
localStateRepository.internetPrivacyMode
) { isQuickPrivacyEnabled, internetPrivacyMode ->
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
index 0d25d16..c7c434c 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt
@@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -48,7 +47,7 @@ class IpScramblingStateUseCase(
val internetPrivacyMode: StateFlow<InternetPrivacyMode> = callbackFlow {
val listener = object : IIpScramblerModule.Listener {
override fun onStatusChanged(newStatus: IIpScramblerModule.Status) {
- offer(map(newStatus))
+ trySend(map(newStatus))
}
override fun log(message: String) {}
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
index 3319eb0..6417fce 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt
@@ -61,6 +61,10 @@ class TrackersStateUseCase(
return appListsRepository.getApplicationDescription(packageName)
}
+ fun getApplicationDescription(appUid: Int): ApplicationDescription? {
+ return appListsRepository.getApplicationDescription(appUid)
+ }
+
fun isWhitelisted(appUid: Int): Boolean {
return if (appUid == appListsRepository.dummySystemApp.uid) {
appListsRepository.getHiddenSystemApps().any {
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
index 1fddb74..5abe0b8 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt
@@ -19,6 +19,7 @@ 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
@@ -26,14 +27,18 @@ import foundation.e.privacymodules.permissions.data.ApplicationDescription
import foundation.e.privacymodules.trackers.IBlockTrackersPrivacyModule
import foundation.e.privacymodules.trackers.ITrackTrackersPrivacyModule
import foundation.e.privacymodules.trackers.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,
@@ -45,16 +50,21 @@ class TrackersStatisticsUseCase(
appListsRepository.getVisibleApps()
}
- fun listenUpdates(): Flow<Unit> = callbackFlow {
+ private fun rawUpdates(): Flow<Unit> = callbackFlow {
val listener = object : ITrackTrackersPrivacyModule.Listener {
override fun onNewData() {
- offer(Unit)
+ trySend(Unit)
}
}
trackTrackersPrivacyModule.addListener(listener)
awaitClose { trackTrackersPrivacyModule.removeListener(listener) }
}
+ @OptIn(FlowPreview::class)
+ fun listenUpdates(debounce: Duration = 1.seconds) = rawUpdates()
+ .throttleFirst(windowDuration = debounce)
+ .onStart { emit(Unit) }
+
fun getDayStatistics(): Pair<TrackersPeriodicStatistics, Int> {
return TrackersPeriodicStatistics(
callsBlockedNLeaked = trackTrackersPrivacyModule.getPastDayTrackersCalls(),
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt
deleted file mode 100644
index 95a8cfe..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt
+++ /dev/null
@@ -1,233 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.privacycentralapp.features.dashboard
-
-import android.util.Log
-import foundation.e.flowmvi.Actor
-import foundation.e.flowmvi.Reducer
-import foundation.e.flowmvi.SingleEventProducer
-import foundation.e.flowmvi.feature.BaseFeature
-import foundation.e.privacycentralapp.R
-import foundation.e.privacycentralapp.domain.entities.LocationMode
-import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState
-import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase
-import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase
-import foundation.e.privacymodules.permissions.data.ApplicationDescription
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-
-// Define a state machine for Dashboard Feature
-class DashboardFeature(
- initialState: State,
- coroutineScope: CoroutineScope,
- reducer: Reducer<State, Effect>,
- actor: Actor<State, Action, Effect>,
- singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
-) : BaseFeature<DashboardFeature.State,
- DashboardFeature.Action,
- DashboardFeature.Effect,
- DashboardFeature.SingleEvent>(
- initialState, actor, reducer, coroutineScope, { message -> Log.d("DashboardFeature", message) },
- singleEventProducer
-) {
- data class State(
- val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED,
- val isTrackersDenied: Boolean = false,
- val isLocationHidden: Boolean = false,
- val isIpHidden: Boolean? = false,
- val locationMode: LocationMode = LocationMode.REAL_LOCATION,
- val leakedTrackersCount: Int? = null,
- val trackersCount: Int? = null,
- val allowedTrackersCount: Int? = null,
- val dayStatistics: List<Pair<Int, Int>>? = null,
- val dayLabels: List<String>? = null,
- val showQuickPrivacyDisabledMessage: Boolean = false
- )
-
- sealed class SingleEvent {
- object NavigateToTrackersSingleEvent : SingleEvent()
- object NavigateToInternetActivityPrivacySingleEvent : SingleEvent()
- object NavigateToLocationSingleEvent : SingleEvent()
- object NavigateToPermissionsSingleEvent : SingleEvent()
- data class NavigateToAppDetailsEvent(val appDesc: ApplicationDescription) : SingleEvent()
- object NewStatisticsAvailableSingleEvent : SingleEvent()
- data class ToastMessageSingleEvent(val message: Int) : SingleEvent()
- }
-
- sealed class Action {
- object InitAction : Action()
- object TogglePrivacyAction : Action()
- object ShowFakeMyLocationAction : Action()
- object ShowInternetActivityPrivacyAction : Action()
- object ShowAppsPermissions : Action()
- object ShowTrackers : Action()
- object FetchStatistics : Action()
- object CloseQuickPrivacyDisabledMessage : Action()
- object ShowMostLeakedApp : Action()
- }
-
- sealed class Effect {
- object NoEffect : Effect()
- data class UpdateStateEffect(val state: QuickPrivacyState) : Effect()
- data class IpScramblingModeUpdatedEffect(val isIpHidden: Boolean?) : Effect()
- data class TrackersStatisticsUpdatedEffect(
- val dayStatistics: List<Pair<Int, Int>>,
- val dayLabels: List<String>,
- val dayTrackersCount: Int,
- val trackersCount: Int,
- val allowedTrackersCount: Int
- ) : Effect()
- data class TrackersBlockedUpdatedEffect(val areAllTrackersBlocked: Boolean) : Effect()
- data class UpdateLocationModeEffect(val mode: LocationMode) : Effect()
- object OpenFakeMyLocationEffect : Effect()
- object OpenInternetActivityPrivacyEffect : Effect()
- object OpenAppsPermissionsEffect : Effect()
- object OpenTrackersEffect : Effect()
- object NewStatisticsAvailablesEffect : Effect()
- object FirstIPTrackerActivationEffect : Effect()
- data class LocationHiddenUpdatedEffect(val isLocationHidden: Boolean) : Effect()
- data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect()
- data class OpenAppDetailsEffect(val appDesc: ApplicationDescription) : Effect()
- }
-
- companion object {
- fun create(
- coroutineScope: CoroutineScope,
- getPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- trackersStatisticsUseCase: TrackersStatisticsUseCase,
- ): DashboardFeature =
- DashboardFeature(
- initialState = State(),
- coroutineScope,
- reducer = { state, effect ->
- when (effect) {
- is Effect.UpdateStateEffect -> state.copy(quickPrivacyState = effect.state)
- is Effect.IpScramblingModeUpdatedEffect -> state.copy(isIpHidden = effect.isIpHidden)
- is Effect.TrackersStatisticsUpdatedEffect -> state.copy(
- dayStatistics = effect.dayStatistics,
- dayLabels = effect.dayLabels,
- leakedTrackersCount = effect.dayTrackersCount,
- trackersCount = effect.trackersCount,
- allowedTrackersCount = effect.allowedTrackersCount
- )
-
- is Effect.TrackersBlockedUpdatedEffect -> state.copy(
- isTrackersDenied = effect.areAllTrackersBlocked
- )
- is Effect.LocationHiddenUpdatedEffect -> state.copy(
- isLocationHidden = effect.isLocationHidden
- )
- is Effect.UpdateLocationModeEffect -> state.copy(locationMode = effect.mode)
- is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show)
- else -> state
- }
- },
- actor = { _: State, action: Action ->
- when (action) {
- Action.TogglePrivacyAction -> {
- val isFirstActivation = getPrivacyStateUseCase.toggleReturnIsFirstActivation()
- flow {
- emit(Effect.NewStatisticsAvailablesEffect)
- if (isFirstActivation) emit(Effect.FirstIPTrackerActivationEffect)
- }
- }
-
- Action.InitAction -> {
- trackersStatisticsUseCase.initAppList()
- merge(
- getPrivacyStateUseCase.quickPrivacyState.map {
- Effect.UpdateStateEffect(it)
- },
- getPrivacyStateUseCase.isIpHidden.map {
- Effect.IpScramblingModeUpdatedEffect(it)
- },
- trackersStatisticsUseCase.listenUpdates().map {
- Effect.NewStatisticsAvailablesEffect
- },
- getPrivacyStateUseCase.isTrackersDenied.map {
- Effect.TrackersBlockedUpdatedEffect(it)
- },
- getPrivacyStateUseCase.isLocationHidden.map {
- Effect.LocationHiddenUpdatedEffect(it)
- },
- getPrivacyStateUseCase.locationMode.map {
- Effect.UpdateLocationModeEffect(it)
- },
- getPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
- Effect.ShowQuickPrivacyDisabledMessageEffect(it)
- },
- )
- }
- Action.ShowFakeMyLocationAction -> flowOf(Effect.OpenFakeMyLocationEffect)
- Action.ShowAppsPermissions -> flowOf(Effect.OpenAppsPermissionsEffect)
- Action.ShowInternetActivityPrivacyAction -> flowOf(
- Effect.OpenInternetActivityPrivacyEffect
- )
- Action.ShowTrackers -> flowOf(Effect.OpenTrackersEffect)
- Action.FetchStatistics ->
- trackersStatisticsUseCase.getNonBlockedTrackersCount()
- .map { nonBlockedTrackersCount ->
- trackersStatisticsUseCase.getDayStatistics()
- .let { (dayStatistics, trackersCount) ->
- Effect.TrackersStatisticsUpdatedEffect(
- dayStatistics = dayStatistics.callsBlockedNLeaked,
- dayLabels = dayStatistics.periods,
- dayTrackersCount = dayStatistics.trackersCount,
- trackersCount = trackersCount,
- allowedTrackersCount = nonBlockedTrackersCount
- )
- }
- }
- is Action.CloseQuickPrivacyDisabledMessage -> {
- getPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
- flowOf(Effect.NoEffect)
- }
- is Action.ShowMostLeakedApp -> {
- Log.d("mostleak", "Action.ShowMostLeakedApp")
- flowOf(
- trackersStatisticsUseCase.getMostLeakedApp()?.let { Effect.OpenAppDetailsEffect(appDesc = it) } ?: Effect.OpenTrackersEffect
- )
- }
- }
- },
- singleEventProducer = { _, _, effect ->
- when (effect) {
- is Effect.OpenFakeMyLocationEffect ->
- SingleEvent.NavigateToLocationSingleEvent
- is Effect.OpenInternetActivityPrivacyEffect ->
- SingleEvent.NavigateToInternetActivityPrivacySingleEvent
- is Effect.OpenAppsPermissionsEffect ->
- SingleEvent.NavigateToPermissionsSingleEvent
- is Effect.OpenTrackersEffect ->
- SingleEvent.NavigateToTrackersSingleEvent
- is Effect.NewStatisticsAvailablesEffect ->
- SingleEvent.NewStatisticsAvailableSingleEvent
- is Effect.FirstIPTrackerActivationEffect ->
- SingleEvent.ToastMessageSingleEvent(
- message = R.string.dashboard_first_ipscrambling_activation
- )
- is Effect.OpenAppDetailsEffect -> SingleEvent.NavigateToAppDetailsEvent(effect.appDesc)
- else -> null
- }
- }
- )
- }
-}
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
index 323f1bb..adb54bb 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt
@@ -26,12 +26,13 @@ import android.widget.Toast
import androidx.core.content.ContextCompat.getColor
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
-import androidx.fragment.app.activityViewModels
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 com.google.android.material.snackbar.Snackbar
-import foundation.e.flowmvi.MVIView
import foundation.e.privacycentralapp.DependencyContainer
import foundation.e.privacycentralapp.PrivacyCentralApplication
import foundation.e.privacycentralapp.R
@@ -41,23 +42,15 @@ import foundation.e.privacycentralapp.common.initQuickPrivacySnackbar
import foundation.e.privacycentralapp.databinding.FragmentDashboardBinding
import foundation.e.privacycentralapp.domain.entities.LocationMode
import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState
-import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf
-import foundation.e.privacycentralapp.features.dashboard.DashboardFeature.State
+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.FlowPreview
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
-@FlowPreview
-class DashboardFragment :
- NavToolbarFragment(R.layout.fragment_dashboard),
- MVIView<DashboardFeature.State, DashboardFeature.Action> {
-
+class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) {
companion object {
private const val PARAM_HIGHLIGHT_INDEX = "PARAM_HIGHLIGHT_INDEX"
fun buildArgs(highlightIndex: Int): Bundle = bundleOf(
@@ -69,8 +62,8 @@ class DashboardFragment :
(this.requireActivity().application as PrivacyCentralApplication).dependencyContainer
}
- private val viewModel: DashboardViewModel by activityViewModels {
- viewModelProviderFactoryOf { dependencyContainer.dashBoardViewModelFactory.create() }
+ private val viewModel: DashboardViewModel by viewModels {
+ dependencyContainer.viewModelsFactory
}
private var graphHolder: GraphHolder? = null
@@ -82,64 +75,10 @@ class DashboardFragment :
private var highlightIndexOnStart: Int? = null
- private var updateUIJob: Job? = null
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
highlightIndexOnStart = arguments?.getInt(PARAM_HIGHLIGHT_INDEX, -1)
-
- updateUIJob = lifecycleScope.launchWhenStarted {
- viewModel.dashboardFeature.takeView(this, this@DashboardFragment)
- }
-
- lifecycleScope.launchWhenStarted {
- viewModel.dashboardFeature.singleEvents.collect { event ->
- when (event) {
- is DashboardFeature.SingleEvent.NavigateToLocationSingleEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<FakeLocationFragment>(R.id.container)
- setReorderingAllowed(true)
- addToBackStack("dashboard")
- }
- }
- is DashboardFeature.SingleEvent.NavigateToInternetActivityPrivacySingleEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<InternetPrivacyFragment>(R.id.container)
- setReorderingAllowed(true)
- addToBackStack("dashboard")
- }
- }
- is DashboardFeature.SingleEvent.NavigateToPermissionsSingleEvent -> {
- val intent = Intent("android.intent.action.MANAGE_PERMISSIONS")
- requireActivity().startActivity(intent)
- }
- DashboardFeature.SingleEvent.NavigateToTrackersSingleEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<TrackersFragment>(R.id.container)
- setReorderingAllowed(true)
- addToBackStack("dashboard")
- }
- }
- is DashboardFeature.SingleEvent.NavigateToAppDetailsEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<AppTrackersFragment>(R.id.container, args = AppTrackersFragment.buildArgs(event.appDesc.label.toString(), event.appDesc.packageName))
- setReorderingAllowed(true)
- addToBackStack("dashboard")
- }
- }
- DashboardFeature.SingleEvent.NewStatisticsAvailableSingleEvent -> {
- viewModel.submitAction(DashboardFeature.Action.FetchStatistics)
- }
- is DashboardFeature.SingleEvent.ToastMessageSingleEvent ->
- Toast.makeText(requireContext(), event.message, Toast.LENGTH_LONG)
- .show()
- }
- }
- }
- lifecycleScope.launchWhenStarted {
- viewModel.submitAction(DashboardFeature.Action.InitAction)
- }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -149,54 +88,99 @@ class DashboardFragment :
graphHolder = GraphHolder(binding.graph, requireContext())
binding.leakingAppButton.setOnClickListener {
- viewModel.submitAction(DashboardFeature.Action.ShowMostLeakedApp)
+ viewModel.submitAction(Action.ShowMostLeakedApp)
}
binding.togglePrivacyCentral.setOnClickListener {
- viewModel.submitAction(DashboardFeature.Action.TogglePrivacyAction)
+ viewModel.submitAction(Action.TogglePrivacyAction)
}
binding.myLocation.container.setOnClickListener {
- viewModel.submitAction(DashboardFeature.Action.ShowFakeMyLocationAction)
+ viewModel.submitAction(Action.ShowFakeMyLocationAction)
}
binding.internetActivityPrivacy.container.setOnClickListener {
- viewModel.submitAction(DashboardFeature.Action.ShowInternetActivityPrivacyAction)
+ viewModel.submitAction(Action.ShowInternetActivityPrivacyAction)
}
binding.appsPermissions.container.setOnClickListener {
- viewModel.submitAction(DashboardFeature.Action.ShowAppsPermissions)
+ viewModel.submitAction(Action.ShowAppsPermissions)
}
binding.amITracked.container.setOnClickListener {
- viewModel.submitAction(DashboardFeature.Action.ShowTrackers)
+ viewModel.submitAction(Action.ShowTrackers)
}
qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) {
- viewModel.submitAction(DashboardFeature.Action.CloseQuickPrivacyDisabledMessage)
+ viewModel.submitAction(Action.CloseQuickPrivacyDisabledMessage)
}
- }
-
- override fun onResume() {
- super.onResume()
- if (updateUIJob == null || updateUIJob?.isActive == false) {
- updateUIJob = lifecycleScope.launch {
- viewModel.dashboardFeature.takeView(this, this@DashboardFragment)
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
}
}
- render(viewModel.dashboardFeature.state.value)
-
- viewModel.submitAction(DashboardFeature.Action.FetchStatistics)
- }
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ when (event) {
+ is SingleEvent.NavigateToLocationSingleEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<FakeLocationFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ is SingleEvent.NavigateToInternetActivityPrivacySingleEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<InternetPrivacyFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ is SingleEvent.NavigateToPermissionsSingleEvent -> {
+ val intent = Intent("android.intent.action.MANAGE_PERMISSIONS")
+ requireActivity().startActivity(intent)
+ }
+ SingleEvent.NavigateToTrackersSingleEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<TrackersFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ is SingleEvent.NavigateToAppDetailsEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<AppTrackersFragment>(
+ R.id.container,
+ args = AppTrackersFragment.buildArgs(
+ event.appDesc.label.toString(),
+ event.appDesc.packageName,
+ event.appDesc.uid
+ )
+ )
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ is SingleEvent.ToastMessageSingleEvent ->
+ Toast.makeText(requireContext(), event.message, Toast.LENGTH_LONG)
+ .show()
+ }
+ }
+ }
+ }
- override fun onPause() {
- super.onPause()
- updateUIJob?.cancel()
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
}
override fun getTitle(): String {
return getString(R.string.dashboard_title)
}
- override fun render(state: State) {
+ private fun render(state: DashboardState) {
if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show()
else qpDisabledSnackbar?.dismiss()
@@ -308,8 +292,6 @@ class DashboardFragment :
binding.executePendingBindings()
}
- override fun actions(): Flow<DashboardFeature.Action> = viewModel.actions
-
override fun onDestroyView() {
super.onDestroyView()
qpDisabledSnackbar = 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
new file mode 100644
index 0000000..65aa444
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 E FOUNDATION
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.privacycentralapp.features.dashboard
+
+import foundation.e.privacycentralapp.domain.entities.LocationMode
+import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState
+
+data class DashboardState(
+ val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED,
+ val isTrackersDenied: Boolean = false,
+ val isLocationHidden: Boolean = false,
+ val isIpHidden: Boolean? = false,
+ val locationMode: LocationMode = LocationMode.REAL_LOCATION,
+ val leakedTrackersCount: Int? = null,
+ val trackersCount: Int? = null,
+ val allowedTrackersCount: Int? = null,
+ val dayStatistics: List<Pair<Int, Int>>? = null,
+ val dayLabels: List<String>? = null,
+ val showQuickPrivacyDisabledMessage: Boolean = false
+) \ No newline at end of file
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
index ffd7951..e3a9722 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt
@@ -19,41 +19,131 @@ package foundation.e.privacycentralapp.features.dashboard
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.common.Factory
+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.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 _actions = MutableSharedFlow<DashboardFeature.Action>()
- val actions = _actions.asSharedFlow()
+ private val _state = MutableStateFlow(DashboardState())
+ val state = _state.asStateFlow()
- val dashboardFeature: DashboardFeature by lazy {
- DashboardFeature.create(
- coroutineScope = viewModelScope,
- getPrivacyStateUseCase = getPrivacyStateUseCase,
- trackersStatisticsUseCase = trackersStatisticsUseCase,
- )
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) { trackersStatisticsUseCase.initAppList() }
}
- fun submitAction(action: DashboardFeature.Action) {
- viewModelScope.launch {
- _actions.emit(action)
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ merge(
+ getPrivacyStateUseCase.quickPrivacyState.map {
+ _state.update { s -> s.copy(quickPrivacyState = it) }
+ },
+ getPrivacyStateUseCase.isIpHidden.map {
+ _state.update { s -> s.copy(isIpHidden = it) }
+ },
+ trackersStatisticsUseCase.listenUpdates().flatMapLatest {
+ fetchStatistics()
+ },
+ getPrivacyStateUseCase.isTrackersDenied.map {
+ _state.update { s -> s.copy(isTrackersDenied = it) }
+ },
+ getPrivacyStateUseCase.isLocationHidden.map {
+ _state.update { s -> s.copy(isLocationHidden = it) }
+ },
+ getPrivacyStateUseCase.locationMode.map {
+ _state.update { s -> s.copy(locationMode = it) }
+ },
+ getPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
+ _state.update { s -> s.copy(showQuickPrivacyDisabledMessage = it) }
+ }
+ ).collect {}
+ }
+
+ fun submitAction(action: Action) = viewModelScope.launch {
+ when (action) {
+ is Action.TogglePrivacyAction -> actionTogglePrivacy()
+ 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.CloseQuickPrivacyDisabledMessage ->
+ getPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
+ is Action.ShowMostLeakedApp -> actionShowMostLeakedApp()
}
}
-}
-class DashBoardViewModelFactory(
- private val getPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
-) : Factory<DashboardViewModel> {
- override fun create(): DashboardViewModel {
- return DashboardViewModel(getPrivacyStateUseCase, trackersStatisticsUseCase)
+ private suspend fun fetchStatistics(): Flow<Unit> = withContext(Dispatchers.IO) {
+ trackersStatisticsUseCase.getNonBlockedTrackersCount().map { nonBlockedTrackersCount ->
+ trackersStatisticsUseCase.getDayStatistics().let { (dayStatistics, trackersCount) ->
+ _state.update { s ->
+ s.copy(
+ dayStatistics = dayStatistics.callsBlockedNLeaked,
+ dayLabels = dayStatistics.periods,
+ leakedTrackersCount = dayStatistics.trackersCount,
+ trackersCount = trackersCount,
+ allowedTrackersCount = nonBlockedTrackersCount
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun actionTogglePrivacy() = withContext(Dispatchers.IO) {
+ val isFirstActivation = getPrivacyStateUseCase.toggleReturnIsFirstActivation()
+ fetchStatistics().first()
+
+ if (isFirstActivation) _singleEvents.emit(SingleEvent.ToastMessageSingleEvent(
+ message = R.string.dashboard_first_ipscrambling_activation
+ ))
+ }
+
+ 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(val message: Int) : SingleEvent()
+ }
+
+ sealed class Action {
+ object TogglePrivacyAction : Action()
+ object ShowFakeMyLocationAction : Action()
+ object ShowInternetActivityPrivacyAction : Action()
+ object ShowAppsPermissions : Action()
+ object ShowTrackers : Action()
+ object CloseQuickPrivacyDisabledMessage : Action()
+ object ShowMostLeakedApp : Action()
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt
deleted file mode 100644
index 8e4318d..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt
+++ /dev/null
@@ -1,243 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.privacycentralapp.features.internetprivacy
-
-import android.app.Activity
-import android.content.Intent
-import android.util.Log
-import foundation.e.flowmvi.Actor
-import foundation.e.flowmvi.Reducer
-import foundation.e.flowmvi.SingleEventProducer
-import foundation.e.flowmvi.feature.BaseFeature
-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 foundation.e.privacymodules.permissions.data.ApplicationDescription
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.shareIn
-
-// Define a state machine for Internet privacy feature
-class InternetPrivacyFeature(
- initialState: State,
- coroutineScope: CoroutineScope,
- reducer: Reducer<State, Effect>,
- actor: Actor<State, Action, Effect>,
- singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
-) : BaseFeature<InternetPrivacyFeature.State, InternetPrivacyFeature.Action, InternetPrivacyFeature.Effect, InternetPrivacyFeature.SingleEvent>(
- initialState,
- actor,
- reducer,
- coroutineScope,
- { message -> Log.d("InternetPrivacyFeature", message) },
- singleEventProducer
-) {
- data class State(
- val mode: InternetPrivacyMode,
- val availableApps: List<ApplicationDescription>,
- val bypassTorApps: Collection<String>,
- val selectedLocation: String,
- val availableLocationIds: List<String>,
- val forceRedraw: Boolean = false,
- val showQuickPrivacyDisabledMessage: Boolean = false
- ) {
- fun getApps(): List<Pair<ApplicationDescription, Boolean>> {
- return availableApps.map { it to (it.packageName !in bypassTorApps) }
- }
-
- val selectedLocationPosition get() = availableLocationIds.indexOf(selectedLocation)
- }
-
- sealed class SingleEvent {
- data class StartAndroidVpnActivityEvent(val intent: Intent) : SingleEvent()
- data class ErrorEvent(val error: Any) : SingleEvent()
- }
-
- sealed class Action {
- object LoadInternetModeAction : Action()
- object UseRealIPAction : Action()
- object UseHiddenIPAction : Action()
- data class AndroidVpnActivityResultAction(val resultCode: Int) : Action()
- data class ToggleAppIpScrambled(val packageName: String) : Action()
- data class SelectLocationAction(val position: Int) : Action()
- object CloseQuickPrivacyDisabledMessage : Action()
- }
-
- sealed class Effect {
- object NoEffect : Effect()
- data class ModeUpdatedEffect(val mode: InternetPrivacyMode) : Effect()
- data class QuickPrivacyUpdatedEffect(val enabled: Boolean) : Effect()
- object QuickPrivacyDisabledWarningEffect : Effect()
- data class ShowAndroidVpnDisclaimerEffect(val intent: Intent) : Effect()
- data class IpScrambledAppsUpdatedEffect(val bypassTorApps: Collection<String>) : Effect()
- data class AvailableAppsListEffect(
- val apps: List<ApplicationDescription>,
- val bypassTorApps: Collection<String>
- ) : Effect()
- data class LocationSelectedEffect(val locationId: String) : Effect()
- object WarningStartingLongEffect : Effect()
- data class ErrorEffect(val message: String) : Effect()
- data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect()
- }
-
- companion object {
- private const val WARNING_LOADING_LONG_DELAY = 5 * 1000L
- @FlowPreview
- fun create(
- coroutineScope: CoroutineScope,
- ipScramblerModule: IIpScramblerModule,
- getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- ipScramblingStateUseCase: IpScramblingStateUseCase,
- appListUseCase: AppListUseCase,
- availablesLocationsIds: List<String>,
- initialState: State = State(
- mode = ipScramblingStateUseCase.internetPrivacyMode.value,
- availableApps = emptyList(),
- bypassTorApps = emptyList(),
- availableLocationIds = availablesLocationsIds,
- selectedLocation = ""
- )
- ) = InternetPrivacyFeature(
- initialState, coroutineScope,
- reducer = { state, effect ->
- when (effect) {
- is Effect.ModeUpdatedEffect -> state.copy(mode = effect.mode)
- is Effect.IpScrambledAppsUpdatedEffect -> state.copy(bypassTorApps = effect.bypassTorApps)
- is Effect.AvailableAppsListEffect -> state.copy(
- availableApps = effect.apps,
- bypassTorApps = effect.bypassTorApps
- )
- is Effect.LocationSelectedEffect -> state.copy(selectedLocation = effect.locationId)
- Effect.QuickPrivacyDisabledWarningEffect -> state.copy(forceRedraw = !state.forceRedraw)
- is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show)
- else -> state
- }
- },
- actor = { state, action ->
- when {
- action is Action.LoadInternetModeAction -> merge(
- getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow
- .map { Effect.QuickPrivacyUpdatedEffect(it) },
- getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
- Effect.ShowQuickPrivacyDisabledMessageEffect(it)
- },
- getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow.flatMapLatest { enabled ->
- if (enabled) ipScramblingStateUseCase.internetPrivacyMode
- .map { Effect.ModeUpdatedEffect(it) }
- .shareIn(
- scope = coroutineScope,
- started = SharingStarted.Lazily,
- replay = 0
- )
- else ipScramblingStateUseCase.configuredMode.map {
- Effect.ModeUpdatedEffect(
- if (it) InternetPrivacyMode.HIDE_IP
- else InternetPrivacyMode.REAL_IP
- )
- }
- },
- appListUseCase.getAppsUsingInternet().map { apps ->
- Effect.AvailableAppsListEffect(
- apps,
- ipScramblingStateUseCase.bypassTorApps
- )
- },
- flowOf(Effect.LocationSelectedEffect(ipScramblerModule.exitCountry)),
- ipScramblingStateUseCase.internetPrivacyMode
- .map { it == InternetPrivacyMode.HIDE_IP_LOADING }
- .debounce(WARNING_LOADING_LONG_DELAY)
- .map { if (it) Effect.WarningStartingLongEffect else Effect.NoEffect }
- ).flowOn(Dispatchers.Default)
- action is Action.AndroidVpnActivityResultAction ->
- if (action.resultCode == Activity.RESULT_OK) {
- if (state.mode in listOf(
- InternetPrivacyMode.REAL_IP,
- InternetPrivacyMode.REAL_IP_LOADING
- )
- ) {
- ipScramblingStateUseCase.toggle(hideIp = true)
- flowOf(Effect.ModeUpdatedEffect(InternetPrivacyMode.HIDE_IP_LOADING))
- } else {
- flowOf(Effect.ErrorEffect("Vpn already started"))
- }
- } else {
- flowOf(Effect.ErrorEffect("Vpn wasn't allowed to start"))
- }
-
- action is Action.UseRealIPAction && state.mode in listOf(
- InternetPrivacyMode.HIDE_IP,
- InternetPrivacyMode.HIDE_IP_LOADING,
- InternetPrivacyMode.REAL_IP_LOADING
- ) -> {
- ipScramblingStateUseCase.toggle(hideIp = false)
- flowOf(Effect.ModeUpdatedEffect(InternetPrivacyMode.REAL_IP_LOADING))
- }
- action is Action.UseHiddenIPAction
- && state.mode in listOf(
- InternetPrivacyMode.REAL_IP,
- InternetPrivacyMode.REAL_IP_LOADING
- ) -> {
- ipScramblingStateUseCase.toggle(hideIp = true)
- flowOf(Effect.ModeUpdatedEffect(InternetPrivacyMode.HIDE_IP_LOADING))
- }
-
- action is Action.ToggleAppIpScrambled -> {
- ipScramblingStateUseCase.toggleBypassTor(action.packageName)
- flowOf(Effect.IpScrambledAppsUpdatedEffect(bypassTorApps = ipScramblingStateUseCase.bypassTorApps))
- }
- action is Action.SelectLocationAction -> {
- val locationId = state.availableLocationIds[action.position]
- if (locationId != ipScramblerModule.exitCountry) {
- ipScramblerModule.exitCountry = locationId
- flowOf(Effect.LocationSelectedEffect(locationId))
- } else {
- flowOf(Effect.NoEffect)
- }
- }
- action is Action.CloseQuickPrivacyDisabledMessage -> {
- getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
- flowOf(Effect.NoEffect)
- }
- else -> flowOf(Effect.NoEffect)
- }
- },
- singleEventProducer = { _, action, effect ->
- when {
- effect is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
- effect is Effect.WarningStartingLongEffect ->
- SingleEvent.ErrorEvent(R.string.ipscrambling_warning_starting_long)
- action is Action.UseHiddenIPAction
- && effect is Effect.ShowAndroidVpnDisclaimerEffect ->
- SingleEvent.StartAndroidVpnActivityEvent(effect.intent)
- else -> null
- }
- }
- )
- }
-}
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
index 59d30c8..ff8e78f 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
@@ -22,12 +22,12 @@ import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
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.flowmvi.MVIView
import foundation.e.privacycentralapp.DependencyContainer
import foundation.e.privacycentralapp.PrivacyCentralApplication
import foundation.e.privacycentralapp.R
@@ -36,24 +36,18 @@ import foundation.e.privacycentralapp.common.ToggleAppsAdapter
import foundation.e.privacycentralapp.common.initQuickPrivacySnackbar
import foundation.e.privacycentralapp.databinding.FragmentInternetActivityPolicyBinding
import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode
-import foundation.e.privacycentralapp.extensions.toText
-import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
+import foundation.e.privacycentralapp.common.extensions.toText
+import kotlinx.coroutines.launch
import java.util.Locale
-@FlowPreview
-class InternetPrivacyFragment :
- NavToolbarFragment(R.layout.fragment_internet_activity_policy),
- MVIView<InternetPrivacyFeature.State, InternetPrivacyFeature.Action> {
+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 {
- viewModelProviderFactoryOf { dependencyContainer.internetPrivacyViewModelFactory.create() }
+ dependencyContainer.viewModelsFactory
}
private var _binding: FragmentInternetActivityPolicyBinding? = null
@@ -61,37 +55,11 @@ class InternetPrivacyFragment :
private var qpDisabledSnackbar: Snackbar? = null
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- lifecycleScope.launchWhenStarted {
- viewModel.internetPrivacyFeature.takeView(this, this@InternetPrivacyFragment)
- }
- lifecycleScope.launchWhenStarted {
- viewModel.internetPrivacyFeature.singleEvents.collect { event ->
- when (event) {
- is InternetPrivacyFeature.SingleEvent.ErrorEvent -> {
- displayToast(event.error.toText(requireContext()))
- }
- is InternetPrivacyFeature.SingleEvent.StartAndroidVpnActivityEvent -> {
- launchAndroidVpnDisclaimer.launch(event.intent)
- }
- }
- }
- }
- lifecycleScope.launchWhenStarted {
- viewModel.submitAction(InternetPrivacyFeature.Action.LoadInternetModeAction)
- }
- }
-
private fun displayToast(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
.show()
}
- private val launchAndroidVpnDisclaimer = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- viewModel.submitAction(InternetPrivacyFeature.Action.AndroidVpnActivityResultAction(it.resultCode))
- }
-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentInternetActivityPolicyBinding.bind(view)
@@ -101,17 +69,17 @@ class InternetPrivacyFragment :
setHasFixedSize(true)
adapter = ToggleAppsAdapter(R.layout.ipscrambling_item_app_toggle) { packageName ->
viewModel.submitAction(
- InternetPrivacyFeature.Action.ToggleAppIpScrambled(packageName)
+ InternetPrivacyViewModel.Action.ToggleAppIpScrambled(packageName)
)
}
}
binding.radioUseRealIp.radiobutton.setOnClickListener {
- viewModel.submitAction(InternetPrivacyFeature.Action.UseRealIPAction)
+ viewModel.submitAction(InternetPrivacyViewModel.Action.UseRealIPAction)
}
binding.radioUseHiddenIp.radiobutton.setOnClickListener {
- viewModel.submitAction(InternetPrivacyFeature.Action.UseHiddenIPAction)
+ viewModel.submitAction(InternetPrivacyViewModel.Action.UseHiddenIPAction)
}
binding.ipscramblingSelectLocation.apply {
@@ -129,8 +97,17 @@ class InternetPrivacyFragment :
}
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onItemSelected(parentView: AdapterView<*>, selectedItemView: View?, position: Int, id: Long) {
- viewModel.submitAction(InternetPrivacyFeature.Action.SelectLocationAction(position))
+ override fun onItemSelected(
+ parentView: AdapterView<*>,
+ selectedItemView: View?,
+ position: Int,
+ id: Long
+ ) {
+ viewModel.submitAction(
+ InternetPrivacyViewModel.Action.SelectLocationAction(
+ position
+ )
+ )
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
@@ -138,15 +115,37 @@ class InternetPrivacyFragment :
}
qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) {
- viewModel.submitAction(InternetPrivacyFeature.Action.CloseQuickPrivacyDisabledMessage)
+ viewModel.submitAction(InternetPrivacyViewModel.Action.CloseQuickPrivacyDisabledMessage)
}
- binding.executePendingBindings()
+ 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(event.error.toText(requireContext()))
+ }
+ }
+ }
+ }
+ }
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
}
override fun getTitle(): String = getString(R.string.ipscrambling_title)
- override fun render(state: InternetPrivacyFeature.State) {
+ private fun render(state: InternetPrivacyState) {
if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show()
else qpDisabledSnackbar?.dismiss()
@@ -200,8 +199,6 @@ class InternetPrivacyFragment :
}
}
- override fun actions(): Flow<InternetPrivacyFeature.Action> = viewModel.actions
-
override fun onDestroyView() {
super.onDestroyView()
qpDisabledSnackbar = 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
new file mode 100644
index 0000000..25e911f
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 E FOUNDATION
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.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<ApplicationDescription> = emptyList(),
+ val bypassTorApps: Collection<String> = emptyList(),
+ val selectedLocation: String = "",
+ val availableLocationIds: List<String> = emptyList(),
+ val forceRedraw: Boolean = false,
+ val showQuickPrivacyDisabledMessage: Boolean = false
+) {
+ fun getApps(): List<Pair<ApplicationDescription, Boolean>> {
+ return availableApps.map { it to (it.packageName !in bypassTorApps) }
+ }
+
+ val selectedLocationPosition get() = availableLocationIds.indexOf(selectedLocation)
+} \ No newline at end of file
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
index 8bb7d9f..6d083bd 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
@@ -19,15 +19,24 @@ package foundation.e.privacycentralapp.features.internetprivacy
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.common.Factory
+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,
@@ -35,38 +44,110 @@ class InternetPrivacyViewModel(
private val ipScramblingStateUseCase: IpScramblingStateUseCase,
private val appListUseCase: AppListUseCase
) : ViewModel() {
+ companion object {
+ private const val WARNING_LOADING_LONG_DELAY = 5 * 1000L
+ }
+
+ private val _state = MutableStateFlow(InternetPrivacyState())
+ val state = _state.asStateFlow()
+
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
- private val _actions = MutableSharedFlow<InternetPrivacyFeature.Action>()
- val actions = _actions.asSharedFlow()
val availablesLocationsIds = listOf("", *ipScramblerModule.getAvailablesLocations().sorted().toTypedArray())
- @FlowPreview val internetPrivacyFeature: InternetPrivacyFeature by lazy {
- InternetPrivacyFeature.create(
- coroutineScope = viewModelScope,
- ipScramblerModule = ipScramblerModule,
- getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
- ipScramblingStateUseCase = ipScramblingStateUseCase,
- appListUseCase = appListUseCase,
- availablesLocationsIds = availablesLocationsIds
- )
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update { it.copy(
+ mode = ipScramblingStateUseCase.internetPrivacyMode.value,
+ availableLocationIds = availablesLocationsIds,
+ selectedLocation = ipScramblerModule.exitCountry) }
+ }
}
- fun submitAction(action: InternetPrivacyFeature.Action) {
- viewModelScope.launch {
- _actions.emit(action)
+
+ @OptIn(FlowPreview::class)
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ launch {
+ merge(
+ getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
+ _state.update { s -> s.copy(showQuickPrivacyDisabledMessage = it) }
+ },
+ appListUseCase.getAppsUsingInternet().map { apps ->
+ _state.update { s -> s.copy(
+ availableApps = apps,
+ bypassTorApps = ipScramblingStateUseCase.bypassTorApps
+ ) }
+ },
+ if (getQuickPrivacyStateUseCase.isQuickPrivacyEnabled)
+ ipScramblingStateUseCase.internetPrivacyMode.map {
+ _state.update { s -> s.copy(mode = it) }
+ }
+ else ipScramblingStateUseCase.configuredMode.map {
+ _state.update { s -> s.copy(
+ mode = if (it) InternetPrivacyMode.HIDE_IP
+ else InternetPrivacyMode.REAL_IP
+ ) }
+ }
+ ).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)
+ )
+ }
}
}
-}
-class InternetPrivacyViewModelFactory(
- private val ipScramblerModule: IIpScramblerModule,
- private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- private val ipScramblingStateUseCase: IpScramblingStateUseCase,
- private val appListUseCase: AppListUseCase
-) :
- Factory<InternetPrivacyViewModel> {
- override fun create(): InternetPrivacyViewModel {
- return InternetPrivacyViewModel(ipScramblerModule, getQuickPrivacyStateUseCase, ipScramblingStateUseCase, appListUseCase)
+ 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)
+ is Action.CloseQuickPrivacyDisabledMessage ->
+ getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
+ }
+ }
+
+ private fun actionUseRealIP() {
+ ipScramblingStateUseCase.toggle(hideIp = false)
+ }
+
+ private fun actionUseHiddenIP() {
+ ipScramblingStateUseCase.toggle(hideIp = true)
+ }
+
+ suspend private fun actionToggleAppIpScrambled(action: Action.ToggleAppIpScrambled) = withContext(Dispatchers.IO) {
+ ipScramblingStateUseCase.toggleBypassTor(action.packageName)
+ _state.update { it.copy(bypassTorApps = ipScramblingStateUseCase.bypassTorApps) }
+ }
+
+ suspend private 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(val error: Any) : SingleEvent()
+ }
+
+ sealed class Action {
+ object UseRealIPAction : Action()
+ object UseHiddenIPAction : Action()
+ data class ToggleAppIpScrambled(val packageName: String) : Action()
+ data class SelectLocationAction(val position: Int) : Action()
+ object CloseQuickPrivacyDisabledMessage : Action()
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt
deleted file mode 100644
index 85a507d..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt
+++ /dev/null
@@ -1,153 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.privacycentralapp.features.location
-
-import android.location.Location
-import android.util.Log
-import foundation.e.flowmvi.Actor
-import foundation.e.flowmvi.Reducer
-import foundation.e.flowmvi.SingleEventProducer
-import foundation.e.flowmvi.feature.BaseFeature
-import foundation.e.privacycentralapp.domain.entities.LocationMode
-import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase
-import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-
-// Define a state machine for Fake location feature
-class FakeLocationFeature(
- initialState: State,
- coroutineScope: CoroutineScope,
- reducer: Reducer<State, Effect>,
- actor: Actor<State, Action, Effect>,
- singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
-) : BaseFeature<FakeLocationFeature.State, FakeLocationFeature.Action, FakeLocationFeature.Effect, FakeLocationFeature.SingleEvent>(
- initialState,
- actor,
- reducer,
- coroutineScope,
- { message -> Log.d("FakeLocationFeature", message) },
- singleEventProducer
-) {
- data class State(
- val mode: LocationMode = LocationMode.REAL_LOCATION,
- val currentLocation: Location? = null,
- val specificLatitude: Float? = null,
- val specificLongitude: Float? = null,
- val forceRefresh: Boolean = false,
- val showQuickPrivacyDisabledMessage: Boolean = false
- )
-
- sealed class SingleEvent {
- data class LocationUpdatedEvent(val mode: LocationMode, val location: Location?) : SingleEvent()
- data class ErrorEvent(val error: String) : SingleEvent()
- }
-
- sealed class Action {
- object Init : Action()
- object LeaveScreen : Action()
- object UseRealLocationAction : Action()
- object UseRandomLocationAction : Action()
- data class SetSpecificLocationAction(
- val latitude: Float,
- val longitude: Float
- ) : Action()
- object CloseQuickPrivacyDisabledMessage : Action()
- }
-
- sealed class Effect {
- data class LocationModeUpdatedEffect(
- val mode: LocationMode,
- val latitude: Float? = null,
- val longitude: Float? = null
- ) : Effect()
- data class LocationUpdatedEffect(val location: Location?) : Effect()
- data class ErrorEffect(val message: String) : Effect()
- object NoEffect : Effect()
- data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect()
- }
-
- companion object {
- fun create(
- initialState: State = State(),
- getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- fakeLocationStateUseCase: FakeLocationStateUseCase,
- coroutineScope: CoroutineScope
- ) = FakeLocationFeature(
- initialState, coroutineScope,
- reducer = { state, effect ->
- when (effect) {
- is Effect.LocationModeUpdatedEffect -> state.copy(
- mode = effect.mode,
- specificLatitude = effect.latitude,
- specificLongitude = effect.longitude
- )
- is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show)
- else -> state
- }
- },
- actor = { _, action ->
- when (action) {
- is Action.Init -> {
- fakeLocationStateUseCase.startListeningLocation()
- merge(
- fakeLocationStateUseCase.configuredLocationMode.map { (mode, lat, lon) ->
- Effect.LocationModeUpdatedEffect(mode = mode, latitude = lat, longitude = lon)
- },
- fakeLocationStateUseCase.currentLocation.map { Effect.LocationUpdatedEffect(it) },
- getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map { Effect.ShowQuickPrivacyDisabledMessageEffect(it) },
- )
- }
- is Action.LeaveScreen -> {
- fakeLocationStateUseCase.stopListeningLocation()
- flowOf(Effect.NoEffect)
- }
- is Action.SetSpecificLocationAction -> {
- fakeLocationStateUseCase.setSpecificLocation(
- action.latitude,
- action.longitude
- )
- flowOf(Effect.NoEffect)
- }
- is Action.UseRandomLocationAction -> {
- fakeLocationStateUseCase.setRandomLocation()
- flowOf(Effect.NoEffect)
- }
- is Action.UseRealLocationAction -> {
- fakeLocationStateUseCase.stopFakeLocation()
- flowOf(Effect.NoEffect)
- }
- is Action.CloseQuickPrivacyDisabledMessage -> {
- getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
- flowOf(Effect.NoEffect)
- }
- }
- },
- singleEventProducer = { state, _, effect ->
- when (effect) {
- is Effect.LocationUpdatedEffect ->
- SingleEvent.LocationUpdatedEvent(state.mode, effect.location)
- is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
- else -> null
- }
- }
- )
- }
-}
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
index 284a223..2b858e9 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt
@@ -28,7 +28,9 @@ 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.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
@@ -44,7 +46,6 @@ 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.flowmvi.MVIView
import foundation.e.privacycentralapp.DependencyContainer
import foundation.e.privacycentralapp.PrivacyCentralApplication
import foundation.e.privacycentralapp.R
@@ -52,18 +53,13 @@ import foundation.e.privacycentralapp.common.NavToolbarFragment
import foundation.e.privacycentralapp.common.initQuickPrivacySnackbar
import foundation.e.privacycentralapp.databinding.FragmentFakeLocationBinding
import foundation.e.privacycentralapp.domain.entities.LocationMode
-import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf
-import foundation.e.privacycentralapp.features.location.FakeLocationFeature.Action
+import foundation.e.privacycentralapp.features.location.FakeLocationViewModel.Action
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
-class FakeLocationFragment :
- NavToolbarFragment(R.layout.fragment_fake_location),
- MVIView<FakeLocationFeature.State, Action> {
+class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) {
private var isFirstLaunch: Boolean = true
@@ -72,7 +68,7 @@ class FakeLocationFragment :
}
private val viewModel: FakeLocationViewModel by viewModels {
- viewModelProviderFactoryOf { dependencyContainer.fakeLocationViewModelFactory.create() }
+ dependencyContainer.viewModelsFactory
}
private var _binding: FragmentFakeLocationBinding? = null
@@ -87,26 +83,6 @@ class FakeLocationFragment :
companion object {
private const val DEBOUNCE_PERIOD = 1000L
- private const val DEFAULT_INTERVAL_IN_MILLISECONDS = 1000L
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- lifecycleScope.launchWhenStarted {
- viewModel.fakeLocationFeature.takeView(this, this@FakeLocationFragment)
- }
- lifecycleScope.launchWhenStarted {
- viewModel.fakeLocationFeature.singleEvents.collect { event ->
- when (event) {
- is FakeLocationFeature.SingleEvent.ErrorEvent -> {
- displayToast(event.error)
- }
- is FakeLocationFeature.SingleEvent.LocationUpdatedEvent -> {
- updateLocation(event.location, event.mode)
- }
- }
- }
- }
}
override fun onAttach(context: Context) {
@@ -146,13 +122,41 @@ class FakeLocationFragment :
// Bind click listeners once map is ready.
bindClickListeners()
- render(viewModel.fakeLocationFeature.state.value)
+ render(viewModel.state.value)
}
}
qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) {
viewModel.submitAction(Action.CloseQuickPrivacyDisabledMessage)
}
+
+ 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.LocationUpdatedEvent -> {
+ updateLocation(event.location, event.mode)
+ }
+ }
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
}
private fun getCoordinatesAfterTextChanged(
@@ -231,7 +235,7 @@ class FakeLocationFragment :
}
@SuppressLint("MissingPermission")
- override fun render(state: FakeLocationFeature.State) {
+ private fun render(state: FakeLocationState) {
if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show()
else qpDisabledSnackbar?.dismiss()
@@ -267,8 +271,6 @@ class FakeLocationFragment :
binding.edittextLongitude.setText(state.specificLongitude?.toString())
}
- override fun actions(): Flow<Action> = viewModel.actions
-
@SuppressLint("MissingPermission")
private fun updateLocation(lastLocation: Location?, mode: LocationMode) {
lastLocation?.let { location ->
@@ -324,7 +326,7 @@ class FakeLocationFragment :
override fun onResume() {
super.onResume()
- viewModel.submitAction(Action.Init)
+ viewModel.submitAction(Action.EnterScreen)
binding.mapView.onResume()
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/extensions/ViewModelExtension.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt
index d256219..c7bcd98 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/extensions/ViewModelExtension.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 E FOUNDATION
+ * 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
@@ -15,14 +15,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package foundation.e.privacycentralapp.extensions
+package foundation.e.privacycentralapp.features.location
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
+import android.location.Location
+import foundation.e.privacycentralapp.domain.entities.LocationMode
-inline fun <VM : ViewModel> viewModelProviderFactoryOf(
- crossinline f: () -> VM
-): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
- @Suppress("UNCHECKED_CAST")
- override fun <T : ViewModel?> create(modelClass: Class<T>): T = f() as T
-}
+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,
+ val showQuickPrivacyDisabledMessage: Boolean = false
+) \ No newline at end of file
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
index 4b91276..af20a72 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt
@@ -17,43 +17,104 @@
package foundation.e.privacycentralapp.features.location
+import android.location.Location
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.common.Factory
+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 _actions = MutableSharedFlow<FakeLocationFeature.Action>()
- val actions = _actions.asSharedFlow()
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
- val fakeLocationFeature: FakeLocationFeature by lazy {
- FakeLocationFeature.create(
- getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
- fakeLocationStateUseCase = fakeLocationStateUseCase,
- coroutineScope = viewModelScope
- )
+ private val specificLocationInputFlow = MutableSharedFlow<Action.SetSpecificLocationAction>()
+
+ @OptIn(FlowPreview::class)
+ suspend fun doOnStartedState() = withContext(Dispatchers.Main) {
+ launch {
+ merge(
+ fakeLocationStateUseCase.configuredLocationMode.map { (mode, lat, lon) ->
+ _state.update { s -> s.copy(
+ mode = mode,
+ specificLatitude = lat,
+ specificLongitude = lon
+ ) }
+ },
+ getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
+ _state.update { s -> s.copy(showQuickPrivacyDisabledMessage = it) }
+ },
+ 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: FakeLocationFeature.Action) {
- viewModelScope.launch {
- _actions.emit(action)
+ fun submitAction(action: Action) = viewModelScope.launch {
+ when (action) {
+ is Action.EnterScreen -> fakeLocationStateUseCase.startListeningLocation()
+ is Action.LeaveScreen -> fakeLocationStateUseCase.stopListeningLocation()
+ is Action.SetSpecificLocationAction -> setSpecificLocation(action)
+ is Action.UseRandomLocationAction -> fakeLocationStateUseCase.setRandomLocation()
+ is Action.UseRealLocationAction ->
+ fakeLocationStateUseCase.stopFakeLocation()
+ is Action.CloseQuickPrivacyDisabledMessage ->
+ getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
}
}
-}
-class FakeLocationViewModelFactory(
- private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- private val fakeLocationStateUseCase: FakeLocationStateUseCase
-) : Factory<FakeLocationViewModel> {
- override fun create(): FakeLocationViewModel {
- return FakeLocationViewModel(getQuickPrivacyStateUseCase, fakeLocationStateUseCase)
+ private suspend fun setSpecificLocation(action: Action.SetSpecificLocationAction) {
+ specificLocationInputFlow.emit(action)
+ }
+
+ sealed class SingleEvent {
+ data class LocationUpdatedEvent(val mode: LocationMode, val location: Location?) : SingleEvent()
+ data class ErrorEvent(val error: String) : SingleEvent()
+ }
+
+ sealed class Action {
+ object EnterScreen : Action()
+ object LeaveScreen : Action()
+ object UseRealLocationAction : Action()
+ object UseRandomLocationAction : Action()
+ data class SetSpecificLocationAction(
+ val latitude: Float,
+ val longitude: Float
+ ) : Action()
+ object CloseQuickPrivacyDisabledMessage : Action()
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt
deleted file mode 100644
index 25443e9..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt
+++ /dev/null
@@ -1,158 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.privacycentralapp.features.trackers
-
-import android.util.Log
-import foundation.e.flowmvi.Actor
-import foundation.e.flowmvi.Reducer
-import foundation.e.flowmvi.SingleEventProducer
-import foundation.e.flowmvi.feature.BaseFeature
-import foundation.e.privacycentralapp.domain.entities.AppWithCounts
-import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics
-import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase
-import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-
-// Define a state machine for Tracker feature.
-class TrackersFeature(
- initialState: State,
- coroutineScope: CoroutineScope,
- reducer: Reducer<State, Effect>,
- actor: Actor<State, Action, Effect>,
- singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
-) : BaseFeature<TrackersFeature.State, TrackersFeature.Action, TrackersFeature.Effect, TrackersFeature.SingleEvent>(
- initialState,
- actor,
- reducer,
- coroutineScope,
- { message -> Log.d("TrackersFeature", message) },
- singleEventProducer
-) {
- data class State(
- val dayStatistics: TrackersPeriodicStatistics? = null,
- val monthStatistics: TrackersPeriodicStatistics? = null,
- val yearStatistics: TrackersPeriodicStatistics? = null,
- val apps: List<AppWithCounts>? = null,
- val showQuickPrivacyDisabledMessage: Boolean = false
- )
-
- sealed class SingleEvent {
- data class ErrorEvent(val error: String) : SingleEvent()
- data class OpenAppDetailsEvent(val appDesc: AppWithCounts) : SingleEvent()
- object NewStatisticsAvailableSingleEvent : SingleEvent()
- }
-
- sealed class Action {
- object InitAction : Action()
- data class ClickAppAction(val packageName: String) : Action()
- object FetchStatistics : Action()
- object CloseQuickPrivacyDisabledMessage : Action()
- }
-
- sealed class Effect {
- object NoEffect : Effect()
- data class TrackersStatisticsLoadedEffect(
- val dayStatistics: TrackersPeriodicStatistics? = null,
- val monthStatistics: TrackersPeriodicStatistics? = null,
- val yearStatistics: TrackersPeriodicStatistics? = null
- ) : Effect()
- data class AvailableAppsListEffect(
- val apps: List<AppWithCounts>
- ) : Effect()
- data class OpenAppDetailsEffect(val appDesc: AppWithCounts) : Effect()
- data class ErrorEffect(val message: String) : Effect()
- object NewStatisticsAvailablesEffect : Effect()
- data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect()
- }
-
- companion object {
- fun create(
- initialState: State = State(),
- getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- coroutineScope: CoroutineScope,
- trackersStatisticsUseCase: TrackersStatisticsUseCase
- ) = TrackersFeature(
- initialState, coroutineScope,
- reducer = { state, effect ->
- when (effect) {
- is Effect.TrackersStatisticsLoadedEffect -> state.copy(
- dayStatistics = effect.dayStatistics,
- monthStatistics = effect.monthStatistics,
- yearStatistics = effect.yearStatistics,
- )
- is Effect.AvailableAppsListEffect -> state.copy(apps = effect.apps)
-
- is Effect.ErrorEffect -> state
- is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show)
- else -> state
- }
- },
- actor = { state, action ->
- when (action) {
- Action.InitAction -> merge<Effect>(
- trackersStatisticsUseCase.listenUpdates().map {
- Effect.NewStatisticsAvailablesEffect
- },
- getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
- Effect.ShowQuickPrivacyDisabledMessageEffect(it)
- },
- )
-
- is Action.ClickAppAction -> flowOf(
- state.apps?.find { it.packageName == action.packageName }?.let {
- Effect.OpenAppDetailsEffect(it)
- } ?: run { Effect.ErrorEffect("Can't find back app.") }
- )
- is Action.FetchStatistics -> merge<Effect>(
- flow {
- trackersStatisticsUseCase.getDayMonthYearStatistics()
- .let { (day, month, year) ->
- emit(
- Effect.TrackersStatisticsLoadedEffect(
- dayStatistics = day,
- monthStatistics = month,
- yearStatistics = year,
- )
- )
- }
- },
- trackersStatisticsUseCase.getAppsWithCounts().map {
- Effect.AvailableAppsListEffect(it)
- }
- )
- is Action.CloseQuickPrivacyDisabledMessage -> {
- getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
- flowOf(Effect.NoEffect)
- }
- }
- },
- singleEventProducer = { _, _, effect ->
- when (effect) {
- is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
- is Effect.OpenAppDetailsEffect -> SingleEvent.OpenAppDetailsEvent(effect.appDesc)
- is Effect.NewStatisticsAvailablesEffect -> SingleEvent.NewStatisticsAvailableSingleEvent
- else -> null
- }
- }
- )
- }
-}
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
index f6a031b..4992230 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt
@@ -24,10 +24,11 @@ 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 com.google.android.material.snackbar.Snackbar
-import foundation.e.flowmvi.MVIView
import foundation.e.privacycentralapp.DependencyContainer
import foundation.e.privacycentralapp.PrivacyCentralApplication
import foundation.e.privacycentralapp.R
@@ -38,22 +39,17 @@ import foundation.e.privacycentralapp.common.initQuickPrivacySnackbar
import foundation.e.privacycentralapp.databinding.FragmentTrackersBinding
import foundation.e.privacycentralapp.databinding.TrackersItemGraphBinding
import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics
-import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf
import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFragment
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
class TrackersFragment :
- NavToolbarFragment(R.layout.fragment_trackers),
- MVIView<TrackersFeature.State, TrackersFeature.Action> {
+ NavToolbarFragment(R.layout.fragment_trackers) {
private val dependencyContainer: DependencyContainer by lazy {
(this.requireActivity().application as PrivacyCentralApplication).dependencyContainer
}
- private val viewModel: TrackersViewModel by viewModels {
- viewModelProviderFactoryOf { dependencyContainer.trackersViewModelFactory.create() }
- }
+ private val viewModel: TrackersViewModel by viewModels { dependencyContainer.viewModelsFactory }
private var _binding: FragmentTrackersBinding? = null
private val binding get() = _binding!!
@@ -63,41 +59,6 @@ class TrackersFragment :
private var yearGraphHolder: GraphHolder? = null
private var qpDisabledSnackbar: Snackbar? = null
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- lifecycleScope.launchWhenStarted {
- viewModel.trackersFeature.takeView(this, this@TrackersFragment)
- }
- lifecycleScope.launchWhenStarted {
- viewModel.trackersFeature.singleEvents.collect { event ->
- when (event) {
- is TrackersFeature.SingleEvent.ErrorEvent -> {
- displayToast(event.error)
- }
- is TrackersFeature.SingleEvent.OpenAppDetailsEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<AppTrackersFragment>(R.id.container, args = AppTrackersFragment.buildArgs(event.appDesc.label.toString(), event.appDesc.packageName))
- setReorderingAllowed(true)
- addToBackStack("apptrackers")
- }
- }
- is TrackersFeature.SingleEvent.NewStatisticsAvailableSingleEvent -> {
- viewModel.submitAction(TrackersFeature.Action.FetchStatistics)
- }
- }
- }
- }
-
- lifecycleScope.launchWhenStarted {
- viewModel.submitAction(TrackersFeature.Action.InitAction)
- }
- }
-
- private fun displayToast(message: String) {
- Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
- .show()
- }
-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -112,24 +73,63 @@ class TrackersFragment :
setHasFixedSize(true)
adapter = AppsAdapter(R.layout.trackers_item_app) { packageName ->
viewModel.submitAction(
- TrackersFeature.Action.ClickAppAction(packageName)
+ TrackersViewModel.Action.ClickAppAction(packageName)
)
}
}
qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) {
- viewModel.submitAction(TrackersFeature.Action.CloseQuickPrivacyDisabledMessage)
+ viewModel.submitAction(TrackersViewModel.Action.CloseQuickPrivacyDisabledMessage)
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ when (event) {
+ is TrackersViewModel.SingleEvent.ErrorEvent -> {
+ displayToast(event.error)
+ }
+ is TrackersViewModel.SingleEvent.OpenAppDetailsEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ replace<AppTrackersFragment>(
+ R.id.container,
+ args = AppTrackersFragment.buildArgs(
+ event.appDesc.label.toString(),
+ event.appDesc.packageName,
+ event.appDesc.uid
+ )
+ )
+ setReorderingAllowed(true)
+ addToBackStack("apptrackers")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
}
}
- override fun onResume() {
- super.onResume()
- viewModel.submitAction(TrackersFeature.Action.FetchStatistics)
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
}
override fun getTitle() = getString(R.string.trackers_title)
- override fun render(state: TrackersFeature.State) {
+ private fun render(state: TrackersState) {
if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show()
else qpDisabledSnackbar?.dismiss()
@@ -162,8 +162,6 @@ class TrackersFragment :
}
}
- override fun actions(): Flow<TrackersFeature.Action> = viewModel.actions
-
override fun onDestroyView() {
super.onDestroyView()
qpDisabledSnackbar = null
@@ -171,6 +169,5 @@ class TrackersFragment :
monthGraphHolder = null
yearGraphHolder = null
_binding = null
-
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/main/MainViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt
index 7e758b7..f51ff18 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/main/MainViewModel.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 E FOUNDATION
+ * 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
@@ -15,8 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package foundation.e.privacycentralapp.main
+package foundation.e.privacycentralapp.features.trackers
-import androidx.lifecycle.ViewModel
+import foundation.e.privacycentralapp.domain.entities.AppWithCounts
+import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics
-class MainViewModel : ViewModel()
+data class TrackersState(
+ val dayStatistics: TrackersPeriodicStatistics? = null,
+ val monthStatistics: TrackersPeriodicStatistics? = null,
+ val yearStatistics: TrackersPeriodicStatistics? = null,
+ val apps: List<AppWithCounts>? = null,
+ val showQuickPrivacyDisabledMessage: Boolean = false
+) \ No newline at end of file
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
index 4140381..f49152e 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt
@@ -19,45 +19,74 @@ package foundation.e.privacycentralapp.features.trackers
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.common.Factory
+import foundation.e.privacycentralapp.domain.entities.AppWithCounts
import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase
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 getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
private val trackersStatisticsUseCase: TrackersStatisticsUseCase
) : ViewModel() {
- private val _actions = MutableSharedFlow<TrackersFeature.Action>()
- val actions = _actions.asSharedFlow()
+ private val _state = MutableStateFlow(TrackersState())
+ val state = _state.asStateFlow()
- val trackersFeature: TrackersFeature by lazy {
- TrackersFeature.create(
- coroutineScope = viewModelScope,
- getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
- trackersStatisticsUseCase = trackersStatisticsUseCase
- )
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ merge(
+ getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
+ _state.update { s -> s.copy(showQuickPrivacyDisabledMessage = it) }
+ },
+ 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: TrackersFeature.Action) {
- viewModelScope.launch {
- _actions.emit(action)
+ fun submitAction(action: Action) = viewModelScope.launch {
+ when (action) {
+ is Action.ClickAppAction -> actionClickApp(action)
+ is Action.CloseQuickPrivacyDisabledMessage -> {
+ getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
+ }
}
}
-}
-class TrackersViewModelFactory(
- private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- private val trackersStatisticsUseCase: TrackersStatisticsUseCase
-) :
- Factory<TrackersViewModel> {
- override fun create(): TrackersViewModel {
- return TrackersViewModel(
- getQuickPrivacyStateUseCase,
- trackersStatisticsUseCase
- )
+ suspend private fun actionClickApp(action: Action.ClickAppAction) {
+ state.value.apps?.find { it.packageName == action.packageName }?.let {
+ _singleEvents.emit(SingleEvent.OpenAppDetailsEvent(it))
+ }
+ }
+
+ sealed class SingleEvent {
+ data class ErrorEvent(val error: String) : SingleEvent()
+ data class OpenAppDetailsEvent(val appDesc: AppWithCounts) : SingleEvent()
+ }
+
+ sealed class Action {
+ data class ClickAppAction(val packageName: String) : Action()
+ object CloseQuickPrivacyDisabledMessage : Action()
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt
deleted file mode 100644
index f6d7d67..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt
+++ /dev/null
@@ -1,242 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.privacycentralapp.features.trackers.apptrackers
-
-import android.net.Uri
-import android.util.Log
-import foundation.e.flowmvi.Actor
-import foundation.e.flowmvi.Reducer
-import foundation.e.flowmvi.SingleEventProducer
-import foundation.e.flowmvi.feature.BaseFeature
-import foundation.e.privacycentralapp.R
-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.Tracker
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-
-// Define a state machine for Tracker feature.
-class AppTrackersFeature(
- initialState: State,
- coroutineScope: CoroutineScope,
- reducer: Reducer<State, Effect>,
- actor: Actor<State, Action, Effect>,
- singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
-) : BaseFeature<AppTrackersFeature.State, AppTrackersFeature.Action, AppTrackersFeature.Effect, AppTrackersFeature.SingleEvent>(
- initialState,
- actor,
- reducer,
- coroutineScope,
- { message -> Log.d("TrackersFeature", message) },
- singleEventProducer
-) {
- data class State(
- val appDesc: ApplicationDescription? = null,
- val isBlockingActivated: Boolean = false,
- val trackers: List<Tracker>? = null,
- val whitelist: List<String>? = null,
- val leaked: Int = 0,
- val blocked: Int = 0,
- val isQuickPrivacyEnabled: Boolean = false,
- val showQuickPrivacyDisabledMessage: Boolean = false,
- ) {
- fun getTrackersStatus(): List<Pair<Tracker, Boolean>>? {
- if (trackers != null && whitelist != null) {
- return trackers.map { it to (it.id !in whitelist) }
- } else {
- return null
- }
- }
-
- fun getTrackersCount() = trackers?.size ?: 0
- fun getBlockedTrackersCount(): Int = if (isQuickPrivacyEnabled && isBlockingActivated)
- getTrackersCount() - (whitelist?.size ?: 0)
- else 0
- }
-
- sealed class SingleEvent {
- data class ErrorEvent(val error: Any) : SingleEvent()
- object NewStatisticsAvailableSingleEvent : SingleEvent()
- data class OpenUrlEvent(val url: Uri) : SingleEvent()
- }
-
- sealed class Action {
- data class InitAction(val packageName: String) : 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 FetchStatistics : Action()
- object CloseQuickPrivacyDisabledMessage : Action()
- }
-
- sealed class Effect {
- object NoEffect : Effect()
- data class ErrorEffect(val message: Any) : Effect()
- data class SetAppEffect(val appDesc: ApplicationDescription) : Effect()
- data class AppTrackersBlockingActivatedEffect(val isBlockingActivated: Boolean) : Effect()
- data class AvailableTrackersListEffect(
- val trackers: List<Tracker>,
- val blocked: Int,
- val leaked: Int
- ) : Effect()
- data class TrackersWhitelistUpdateEffect(val whitelist: List<String>) : Effect()
- object NewStatisticsAvailablesEffect : Effect()
- data class QuickPrivacyUpdatedEffect(val enabled: Boolean) : Effect()
- data class OpenUrlEffect(val url: Uri) : Effect()
- data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect()
- }
-
- companion object {
-
- private const val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/fr/trackers/"
- fun create(
- initialState: State = State(),
- coroutineScope: CoroutineScope,
- trackersStateUseCase: TrackersStateUseCase,
- trackersStatisticsUseCase: TrackersStatisticsUseCase,
- getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase
- ) = AppTrackersFeature(
- initialState, coroutineScope,
- reducer = { state, effect ->
- when (effect) {
- is Effect.SetAppEffect -> state.copy(appDesc = effect.appDesc)
- is Effect.AvailableTrackersListEffect -> state.copy(
- trackers = effect.trackers,
- leaked = effect.leaked,
- blocked = effect.blocked
- )
-
- is Effect.AppTrackersBlockingActivatedEffect ->
- state.copy(isBlockingActivated = effect.isBlockingActivated)
-
- is Effect.TrackersWhitelistUpdateEffect ->
- state.copy(whitelist = effect.whitelist)
- is Effect.QuickPrivacyUpdatedEffect ->
- state.copy(isQuickPrivacyEnabled = effect.enabled)
- is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show)
- is Effect.ErrorEffect -> state
- else -> state
- }
- },
- actor = { state, action ->
- when (action) {
- is Action.InitAction ->
- trackersStateUseCase
- .getApplicationDescription(action.packageName)?.let { appDesc ->
- merge<Effect>(
- flow {
- emit(Effect.SetAppEffect(appDesc))
- emit(
- Effect.AppTrackersBlockingActivatedEffect(
- !trackersStateUseCase.isWhitelisted(appDesc.uid)
- )
- )
- emit(
- Effect.TrackersWhitelistUpdateEffect(
- trackersStateUseCase.getTrackersWhitelistIds(appDesc.uid)
- )
- )
- },
- trackersStatisticsUseCase.listenUpdates().map {
- Effect.NewStatisticsAvailablesEffect
- },
- getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow.map {
- Effect.QuickPrivacyUpdatedEffect(it)
- },
- getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
- Effect.ShowQuickPrivacyDisabledMessageEffect(it)
- },
- )
- } ?: flowOf(Effect.ErrorEffect(R.string.apptrackers_error_no_app))
-
- is Action.BlockAllToggleAction ->
- state.appDesc?.uid?.let { appUid ->
- flow {
- trackersStateUseCase.toggleAppWhitelist(appUid, !action.isBlocked)
-
- emit(
- Effect.AppTrackersBlockingActivatedEffect(
- !trackersStateUseCase.isWhitelisted(appUid)
- )
- )
- }
- } ?: run { flowOf(Effect.ErrorEffect("No appDesc.")) }
- is Action.ToggleTrackerAction -> {
- if (state.isBlockingActivated) {
- state.appDesc?.uid?.let { appUid ->
- flow {
- trackersStateUseCase.blockTracker(
- appUid,
- action.tracker,
- action.isBlocked
- )
- emit(
- Effect.TrackersWhitelistUpdateEffect(
- trackersStateUseCase.getTrackersWhitelistIds(appUid)
- )
- )
- }
- } ?: run { flowOf(Effect.ErrorEffect("No appDesc.")) }
- } else flowOf(Effect.NoEffect)
- }
- is Action.ClickTracker -> {
- flowOf(
- action.tracker.exodusId?.let {
- try {
- Effect.OpenUrlEffect(Uri.parse(exodusBaseUrl + it))
- } catch (e: Exception) {
- Effect.ErrorEffect("Invalid Url")
- }
- } ?: Effect.NoEffect
- )
- }
- is Action.FetchStatistics -> flowOf(
- state.appDesc?.uid?.let {
- val (blocked, leaked) = trackersStatisticsUseCase.getCalls(it)
-
- Effect.AvailableTrackersListEffect(
- trackers = trackersStatisticsUseCase.getTrackers(it),
- leaked = leaked,
- blocked = blocked,
- )
- } ?: Effect.ErrorEffect("No appDesc.")
- )
- is Action.CloseQuickPrivacyDisabledMessage -> {
- getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
- flowOf(Effect.NoEffect)
- }
- }
- },
- singleEventProducer = { _, _, effect ->
- when (effect) {
- is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
- is Effect.NewStatisticsAvailablesEffect ->
- SingleEvent.NewStatisticsAvailableSingleEvent
- is Effect.OpenUrlEffect ->
- SingleEvent.OpenUrlEvent(effect.url)
- else -> null
- }
- }
- )
- }
-}
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
index efce9ff..75a9c4a 100644
--- 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
@@ -24,34 +24,33 @@ import android.view.View
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
+import androidx.fragment.app.commit
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.flowmvi.MVIView
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.initQuickPrivacySnackbar
import foundation.e.privacycentralapp.databinding.ApptrackersFragmentBinding
-import foundation.e.privacycentralapp.extensions.toText
-import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf
-import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFeature.Action
-import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFeature.SingleEvent
-import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFeature.State
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
-
-class AppTrackersFragment :
- NavToolbarFragment(R.layout.apptrackers_fragment),
- MVIView<State, Action> {
+import foundation.e.privacycentralapp.common.extensions.toText
+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"
- fun buildArgs(label: String, packageName: String): Bundle = bundleOf(
+
+ 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_PACKAGE_NAME to packageName,
+ PARAM_APP_UID to appUid
)
}
@@ -60,9 +59,7 @@ class AppTrackersFragment :
}
private val viewModel: AppTrackersViewModel by viewModels {
- viewModelProviderFactoryOf {
- dependencyContainer.appTrackersViewModelFactory.create()
- }
+ dependencyContainer.viewModelsFactory
}
private var _binding: ApptrackersFragmentBinding? = null
@@ -72,30 +69,12 @@ class AppTrackersFragment :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- lifecycleScope.launchWhenStarted {
- viewModel.feature.takeView(this, this@AppTrackersFragment)
- }
- lifecycleScope.launchWhenStarted {
- viewModel.feature.singleEvents.collect { event ->
- when (event) {
- is SingleEvent.ErrorEvent ->
- displayToast(event.error.toText(requireContext()))
- is SingleEvent.NewStatisticsAvailableSingleEvent -> {
- viewModel.submitAction(Action.FetchStatistics)
- }
- is SingleEvent.OpenUrlEvent ->
- try {
- startActivity(Intent(Intent.ACTION_VIEW, event.url))
- } catch (e: ActivityNotFoundException) {
- displayToast("No application to see webpages")
- }
- }
- }
- }
- lifecycleScope.launchWhenStarted {
- requireArguments().getString(PARAM_PACKAGE_NAME)?.let {
- viewModel.submitAction(Action.InitAction(it))
+ val appUid = requireArguments().getInt(PARAM_APP_UID, -1)
+ if (appUid == -1) {
+ activity?.supportFragmentManager?.commit(allowStateLoss = true) {
+ remove(this@AppTrackersFragment)
}
+ return
}
}
@@ -111,7 +90,7 @@ class AppTrackersFragment :
_binding = ApptrackersFragmentBinding.bind(view)
binding.blockAllToggle.setOnClickListener {
- viewModel.submitAction(Action.BlockAllToggleAction(binding.blockAllToggle.isChecked))
+ viewModel.submitAction(AppTrackersViewModel.Action.BlockAllToggleAction(binding.blockAllToggle.isChecked))
}
binding.trackers.apply {
@@ -120,23 +99,48 @@ class AppTrackersFragment :
adapter = ToggleTrackersAdapter(
R.layout.apptrackers_item_tracker_toggle,
onToggleSwitch = { tracker, isBlocked ->
- viewModel.submitAction(Action.ToggleTrackerAction(tracker, isBlocked))
+ viewModel.submitAction(AppTrackersViewModel.Action.ToggleTrackerAction(tracker, isBlocked))
},
- onClickTitle = { viewModel.submitAction(Action.ClickTracker(it)) }
+ onClickTitle = { viewModel.submitAction(AppTrackersViewModel.Action.ClickTracker(it)) }
)
}
qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) {
- viewModel.submitAction(Action.CloseQuickPrivacyDisabledMessage)
+ viewModel.submitAction(AppTrackersViewModel.Action.CloseQuickPrivacyDisabledMessage)
}
- }
- override fun onResume() {
- super.onResume()
- viewModel.submitAction(Action.FetchStatistics)
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect { event ->
+ when (event) {
+ is AppTrackersViewModel.SingleEvent.ErrorEvent ->
+ displayToast(event.error.toText(requireContext()))
+ is AppTrackersViewModel.SingleEvent.OpenUrl ->
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, event.url))
+ } catch (e: ActivityNotFoundException) {
+ displayToast("No application to see webpages")
+ }
+ }
+ }
+ }
+ }
+
+ 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)
+ }
+ }
}
- override fun render(state: State) {
+ private fun render(state: AppTrackersState) {
if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show()
else qpDisabledSnackbar?.dismiss()
@@ -174,8 +178,6 @@ class AppTrackersFragment :
}
}
- override fun actions(): Flow<Action> = viewModel.actions
-
override fun onDestroyView() {
super.onDestroyView()
qpDisabledSnackbar = 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
new file mode 100644
index 0000000..9a294e2
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 E FOUNDATION
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.privacycentralapp.features.trackers.apptrackers
+
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import foundation.e.privacymodules.trackers.Tracker
+
+data class AppTrackersState(
+ val appDesc: ApplicationDescription? = null,
+ val isBlockingActivated: Boolean = false,
+ val trackers: List<Tracker>? = null,
+ val whitelist: List<String>? = null,
+ val leaked: Int = 0,
+ val blocked: Int = 0,
+ val isQuickPrivacyEnabled: Boolean = false,
+ val showQuickPrivacyDisabledMessage: Boolean = false,
+) {
+ fun getTrackersStatus(): List<Pair<Tracker, Boolean>>? {
+ if (trackers != null && whitelist != null) {
+ return trackers.map { it to (it.id !in whitelist) }
+ } else {
+ return null
+ }
+ }
+
+ fun getTrackersCount() = trackers?.size ?: 0
+ fun getBlockedTrackersCount(): Int = if (isQuickPrivacyEnabled && isBlockingActivated)
+ getTrackersCount() - (whitelist?.size ?: 0)
+ else 0
+} \ No newline at end of file
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
index 995aa80..eef75a4 100644
--- 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
@@ -17,48 +17,120 @@
package foundation.e.privacycentralapp.features.trackers.apptrackers
+import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.common.Factory
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.trackers.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 appUid: Int,
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/fr/trackers/"
+ }
+
+ private val _state = MutableStateFlow(AppTrackersState())
+ val state = _state.asStateFlow()
+
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
- private val _actions = MutableSharedFlow<AppTrackersFeature.Action>()
- val actions = _actions.asSharedFlow()
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update { it.copy(
+ appDesc = trackersStateUseCase.getApplicationDescription(appUid),
+ isBlockingActivated = !trackersStateUseCase.isWhitelisted(appUid),
+ whitelist = trackersStateUseCase.getTrackersWhitelistIds(appUid),
+ ) }
+ }
+ }
- val feature: AppTrackersFeature by lazy {
- AppTrackersFeature.create(
- coroutineScope = viewModelScope,
- trackersStateUseCase = trackersStateUseCase,
- trackersStatisticsUseCase = trackersStatisticsUseCase,
- getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
- )
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ merge(
+ getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow.map {
+ _state.update { s -> s.copy(isQuickPrivacyEnabled = it) }
+ },
+ getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map {
+ _state.update { s -> s.copy(showQuickPrivacyDisabledMessage = it) }
+ },
+ trackersStatisticsUseCase.listenUpdates().map { fetchStatistics() }
+ ).collect { }
}
- fun submitAction(action: AppTrackersFeature.Action) {
- viewModelScope.launch {
- _actions.emit(action)
+ 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.CloseQuickPrivacyDisabledMessage ->
+ getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage()
+ }
+ }
+
+ private suspend fun blockAllToggleAction(action: Action.BlockAllToggleAction)
+ = withContext(Dispatchers.IO) {
+ trackersStateUseCase.toggleAppWhitelist(appUid, !action.isBlocked)
+ _state.update { it.copy(
+ isBlockingActivated = !trackersStateUseCase.isWhitelisted(appUid)
+ ) }
+ }
+
+ private suspend fun toggleTrackerAction(action: Action.ToggleTrackerAction)
+ = withContext(Dispatchers.IO) {
+ if (state.value.isBlockingActivated) {
+ trackersStateUseCase.blockTracker(appUid, action.tracker, action.isBlocked)
+ _state.update { it.copy(
+ whitelist = trackersStateUseCase.getTrackersWhitelistIds(appUid)
+ ) }
}
}
-}
-class AppTrackersViewModelFactory(
- private val trackersStateUseCase: TrackersStateUseCase,
- private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
- private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase
-) :
- Factory<AppTrackersViewModel> {
- override fun create(): AppTrackersViewModel {
- return AppTrackersViewModel(trackersStateUseCase, trackersStatisticsUseCase, getQuickPrivacyStateUseCase)
+ 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 fun fetchStatistics() {
+ val (blocked, leaked) = trackersStatisticsUseCase.getCalls(appUid)
+ return _state.update { s -> s.copy(
+ trackers = trackersStatisticsUseCase.getTrackers(appUid),
+ leaked = leaked,
+ blocked = blocked,
+ ) }
+ }
+
+
+ sealed class SingleEvent {
+ data class ErrorEvent(val error: Any) : SingleEvent()
+ data class OpenUrl(val url: Uri) : 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 CloseQuickPrivacyDisabledMessage : Action()
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt
index e1ccae8..63ec27f 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt
@@ -30,7 +30,6 @@ import foundation.e.privacycentralapp.features.dashboard.DashboardFragment
import foundation.e.privacycentralapp.features.trackers.TrackersFragment
import kotlinx.coroutines.FlowPreview
-@FlowPreview
open class MainActivity : FragmentActivity(R.layout.activity_main) {
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
diff --git a/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt
index 048b58c..62e279f 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt
@@ -26,6 +26,7 @@ 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
@@ -49,7 +50,7 @@ import java.time.temporal.ChronoUnit
* Implementation of App Widget functionality.
*/
class Widget : AppWidgetProvider() {
- @FlowPreview
+
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
@@ -66,7 +67,6 @@ class Widget : AppWidgetProvider() {
// Enter relevant functionality for when the last widget is disabled
}
- @FlowPreview
companion object {
private var updateWidgetJob: Job? = null
@@ -75,6 +75,7 @@ class Widget : AppWidgetProvider() {
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,
@@ -120,6 +121,7 @@ class Widget : AppWidgetProvider() {
)
}
+ @OptIn(DelicateCoroutinesApi::class)
fun startListening(
appContext: Context,
getPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
@@ -140,7 +142,7 @@ class Widget : AppWidgetProvider() {
}
}
- @FlowPreview
+
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
diff --git a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt
index f95083e..7b8ceb4 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt
@@ -29,10 +29,9 @@ import foundation.e.privacycentralapp.R
import foundation.e.privacycentralapp.Widget
import foundation.e.privacycentralapp.Widget.Companion.isDarkText
import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState
-import foundation.e.privacycentralapp.extensions.dpToPxF
+import foundation.e.privacycentralapp.common.extensions.dpToPxF
import foundation.e.privacycentralapp.main.MainActivity
import foundation.e.privacycentralapp.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_PRIVACY
-import kotlinx.coroutines.FlowPreview
data class State(
val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED,
@@ -43,7 +42,7 @@ data class State(
val activeTrackersCount: Int = 0,
)
-@FlowPreview
+
fun render(
context: Context,
state: State,
@@ -266,7 +265,6 @@ private const val REQUEST_CODE_TOGGLE = 2
private const val REQUEST_CODE_TRACKERS = 3
private const val REQUEST_CODE_HIGHLIGHT = 100
-@FlowPreview
fun applyDarkText(context: Context, state: State, views: RemoteViews) {
views.apply {
listOf(
diff --git a/app/src/main/res/layout/fragment_trackers.xml b/app/src/main/res/layout/fragment_trackers.xml
index 9828215..b6d5b7b 100644
--- a/app/src/main/res/layout/fragment_trackers.xml
+++ b/app/src/main/res/layout/fragment_trackers.xml
@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layout>
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/background"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
-
<include layout="@layout/topbar" />
<androidx.core.widget.NestedScrollView
@@ -15,73 +15,74 @@
android:layout_width="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
- <LinearLayout
- android:layout_height="match_parent"
+
+<LinearLayout
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ >
+ <LinearLayout
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ >
+ <TextView
+ android:id="@+id/trackers_info"
+ android:layout_height="wrap_content"
android:layout_width="match_parent"
- android:orientation="vertical"
- >
- <LinearLayout
- android:layout_height="match_parent"
- android:layout_width="match_parent"
- android:orientation="vertical"
- >
- <TextView
- android:id="@+id/trackers_info"
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:paddingTop="16dp"
- android:paddingHorizontal="16dp"
- android:lineSpacingExtra="5sp"
- android:text="@string/trackers_info"
- />
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/graph_subtitle"
- android:textColor="@color/secondary_text"
- android:textSize="14sp"
- android:paddingTop="24dp"
- android:paddingHorizontal="16dp"
- />
+ android:paddingTop="16dp"
+ android:paddingHorizontal="16dp"
+ android:lineSpacingExtra="5sp"
+ android:text="@string/trackers_info"
+ />
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/graph_subtitle"
+ android:textColor="@color/secondary_text"
+ android:textSize="14sp"
+ android:paddingTop="24dp"
+ android:paddingHorizontal="16dp"
+ />
+
+ <include layout="@layout/trackers_item_graph"
+ android:id="@+id/graph_day"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:period="@{@string/trackers_period_day}"
+ />
+ <include layout="@layout/trackers_item_graph"
+ android:id="@+id/graph_month"
+ android:layout_marginTop="16dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:period="@{@string/trackers_period_month}"
+ />
+ <include layout="@layout/trackers_item_graph"
+ android:id="@+id/graph_year"
+ android:layout_marginTop="16dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:period="@{@string/trackers_period_year}"
+ />
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_marginTop="32dp"
+ android:paddingTop="16dp"
+ android:paddingHorizontal="16dp"
+ android:text="@string/trackers_applist_title"
+ />
+ </LinearLayout>
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/apps"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingBottom="32dp"
+ />
+</LinearLayout>
- <include layout="@layout/trackers_item_graph"
- android:id="@+id/graph_day"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:period="@{@string/trackers_period_day}"
- />
- <include layout="@layout/trackers_item_graph"
- android:id="@+id/graph_month"
- android:layout_marginTop="16dp"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:period="@{@string/trackers_period_month}"
- />
- <include layout="@layout/trackers_item_graph"
- android:id="@+id/graph_year"
- android:layout_marginTop="16dp"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:period="@{@string/trackers_period_year}"
- />
- <TextView
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:layout_marginTop="32dp"
- android:paddingTop="16dp"
- android:paddingHorizontal="16dp"
- android:text="@string/trackers_applist_title"
- />
- </LinearLayout>
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/apps"
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:paddingTop="16dp"
- android:paddingBottom="32dp"
- tools:listitem="@layout/trackers_item_app"
- />
- </LinearLayout>
- </androidx.core.widget.NestedScrollView>
+</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> \ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 980c41a..6216e07 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -30,7 +30,6 @@
<string name="dashboard_state_title_off">Tu privacidad en linea está desprotegida</string>
<string name="dashboard_apps_permissions_subtitle">Gestiona tus permisos</string>
<string name="apptrackers_error_quickprivacy_disabled">Habilitar la \"Privacidad rápida\" para poder activar/desactivar los rastreadores.</string>
- <string name="apptrackers_trackers_count_summary">%1$d rastreadores bloqueados de %2$d rastreadores detectados</string>
<string name="quick_protection_info">La \"Protección rápida\" habilita estos ajustes cuando está activada</string>
<string name="learn_more">Más información</string>
<string name="add_location">Añadir ubicación</string>
@@ -61,7 +60,6 @@
<string name="trackers_graph_hours_period_format">HH:mm</string>
<string name="trackers_graph_days_period_format">EEE d \'de\' MMMM</string>
<string name="trackers_graph_months_period_format">MMMM yyyy</string>
- <string name="trackers_app_trackers_counts">%1$d rastreadores bloqueados de %2$d</string>
<string name="apptrackers_block_all_toggle">Rastreadores bloqueados</string>
<string name="apptrackers_no_trackers_yet_block_off">Todavía no se ha detectado ningún rastreador. Si se detectan nuevos rastreadores se actualizarán aquí.</string>
<string name="ipscrambling_error_quickprivacy_disabled">Habilitada la \"Privacidad rápida\" para utilizar las funcionalidades</string>
diff --git a/build.gradle b/build.gradle
index a6f458d..2442f01 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,9 +4,9 @@ import foundation.e.privacycentral.buildsrc.ReleaseType
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.buildConfig = [
- 'compileSdk': 29,
+ 'compileSdk': 31,
'minSdk' : 26,
- 'targetSdk' : 29,
+ 'targetSdk' : 30,
'version' : [
'major': 1,
'minor': 1,
@@ -31,7 +31,7 @@ buildscript {
dependencies {
classpath Libs.androidGradlePlugin
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -44,6 +44,16 @@ plugins {
}
allprojects {
+ //Support @JvmDefault, and @OptIn
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+ kotlinOptions {
+ freeCompilerArgs = ['-Xjvm-default=enable', '-opt-in=kotlin.RequiresOptIn']
+
+
+ jvmTarget = "1.8"
+ }
+ }
+
repositories {
google()
mavenCentral()
diff --git a/dependencies.gradle b/dependencies.gradle
index 0095881..dcb9f9d 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -19,7 +19,7 @@ libs.leakCanary = "com.squareup.leakcanary:leakcanary-android:2.6"
libs.truth = "com.google.truth:truth:1.1"
-versions.kotlin = "1.6.0"
+versions.kotlin = "1.6.10"
libs.Kotlin = [
stdlib: "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin",
gradlePlugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
@@ -39,7 +39,7 @@ libs.AndroidX = [
coreKtx: "androidx.core:core-ktx:1.5.0-beta01",
]
-versions.fragment = "1.3.3"
+versions.fragment = "1.5.0"
libs.AndroidX.Fragment = [
fragment: "androidx.fragment:fragment:$versions.fragment",
fragmentKtx: "androidx.fragment:fragment-ktx:$versions.fragment",
@@ -53,10 +53,9 @@ libs.AndroidX.Test = [
espresso: "androidx.test.espresso:espresso-core:3.3.0",
]
-versions.lifecycle = "2.3.0-rc01"
+versions.lifecycle = "2.5.0"
libs.AndroidX.Lifecycle = [
runtime: "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle",
- livedata: "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle",
viewmodel: "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle",
]
diff --git a/flow-mvi/.gitignore b/flow-mvi/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/flow-mvi/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build \ No newline at end of file
diff --git a/flow-mvi/build.gradle b/flow-mvi/build.gradle
deleted file mode 100644
index a012229..0000000
--- a/flow-mvi/build.gradle
+++ /dev/null
@@ -1,31 +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 <https://www.gnu.org/licenses/>.
-*/
-
-plugins {
- id 'java-library'
- id 'kotlin'
-}
-
-java {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
-}
-
-dependencies {
- implementation Libs.Kotlin.stdlib
- implementation Libs.Coroutines.core
-}
diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt
deleted file mode 100644
index 3040f3f..0000000
--- a/flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt
+++ /dev/null
@@ -1,24 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.flowmvi
-
-import kotlinx.coroutines.flow.StateFlow
-
-interface Store<State : Any, in Action : Any> {
- val state: StateFlow<State>
-}
diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt
deleted file mode 100644
index 1f22a35..0000000
--- a/flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt
+++ /dev/null
@@ -1,42 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.flowmvi
-
-import kotlinx.coroutines.flow.Flow
-
-/**
- * Actor is a function that receives the current state and the action which just happened
- * and acts on it.
- *
- * It returns a [Flow] of Effects which then can be used in a reducer to reduce to a new state.
- */
-typealias Actor<State, Action, Effect> = (state: State, action: Action) -> Flow<Effect>
-
-/**
- * Reducer is a function that applies the effect to current state and return a new state.
- *
- * This function should be free from any side-effect and make sure it is idempotent.
- */
-typealias Reducer<State, Effect> = (state: State, effect: Effect) -> State
-
-typealias SingleEventProducer<State, Action, Effect, SingleEvent> = (state: State, action: Action, effect: Effect) -> SingleEvent?
-
-/**
- * Logger is function used for logging
- */
-typealias Logger = (String) -> Unit
diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt
deleted file mode 100644
index 1429d1a..0000000
--- a/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt
+++ /dev/null
@@ -1,130 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.flowmvi.feature
-
-import foundation.e.flowmvi.Actor
-import foundation.e.flowmvi.Logger
-import foundation.e.flowmvi.MVIView
-import foundation.e.flowmvi.Reducer
-import foundation.e.flowmvi.SingleEventProducer
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.emitAll
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-
-open class BaseFeature<State : Any, in Action : Any, in Effect : Any, SingleEvent : Any>(
- initialState: State,
- private val actor: Actor<State, Action, Effect>,
- private val reducer: Reducer<State, Effect>,
- private val coroutineScope: CoroutineScope,
- private val defaultLogger: Logger = {},
- private val singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>? = null
-) :
- Feature<State, Action, SingleEvent> {
-
- private val mutex = Mutex()
-
- private val _state = MutableStateFlow(initialState)
- override val state: StateFlow<State> = _state.asStateFlow()
-
- private val singleEventChannel = Channel<SingleEvent>()
- override val singleEvents: Flow<SingleEvent> = singleEventChannel.receiveAsFlow()
-
- override fun takeView(
- viewCoroutineScope: CoroutineScope,
- view: MVIView<State, Action>,
- initialActions: List<Action>,
- logger: Logger?
- ) {
- viewCoroutineScope.launch {
- sendStateUpdatesIntoView(this, view, logger ?: defaultLogger)
- handleViewActions(this, view, initialActions, logger ?: defaultLogger)
- }
- }
-
- private fun sendStateUpdatesIntoView(
- callerCoroutineScope: CoroutineScope,
- view: MVIView<State, Action>,
- @Suppress("UNUSED_PARAMETER") logger: Logger
- ) {
- state
- .onEach {
- view.render(it)
- }
- .launchIn(callerCoroutineScope)
- }
-
- private fun handleViewActions(
- coroutineScope: CoroutineScope,
- view: MVIView<State, Action>,
- initialActions: List<Action>,
- logger: Logger
- ) {
- coroutineScope.launch {
- view
- .actions()
- .onStart {
- emitAll(initialActions.asFlow())
- }
- .collectIntoHandler(this, logger)
- }
- }
-
- override fun addExternalActions(actions: Flow<Action>, logger: Logger?) {
- coroutineScope.launch {
- actions.collectIntoHandler(this, logger ?: defaultLogger)
- }
- }
-
- private suspend fun Flow<Action>.collectIntoHandler(
- callerCoroutineScope: CoroutineScope,
- @Suppress("UNUSED_PARAMETER") logger: Logger
- ) {
- onEach { action ->
- callerCoroutineScope.launch {
- actor.invoke(_state.value, action)
- .onEach { effect ->
- mutex.withLock {
- val newState = reducer.invoke(_state.value, effect)
- _state.value = newState
- singleEventProducer?.also {
- it.invoke(newState, action, effect)?.let { singleEvent ->
- singleEventChannel.send(
- singleEvent
- )
- }
- }
- }
- }
- .launchIn(coroutineScope)
- }
- }
- .launchIn(callerCoroutineScope)
- }
-}
diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt
deleted file mode 100644
index bd9ca16..0000000
--- a/flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt
+++ /dev/null
@@ -1,62 +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 <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.flowmvi.feature
-
-import foundation.e.flowmvi.Logger
-import foundation.e.flowmvi.MVIView
-import foundation.e.flowmvi.Store
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-
-interface Feature<State : Any, in Action : Any, SingleEvent : Any> : Store<State, Action> {
- val singleEvents: Flow<SingleEvent>
-
- /**
- * Call this method when a new [View] is ready to render the state of this MVFlow object.
- *
- * @param viewCoroutineScope the scope of the view. This will be used to launch a coroutine which will run listening
- * to actions until this scope is cancelled.
- * @param view the view that will render the state.
- * @param initialActions an optional list of Actions that can be passed to introduce an initial action into the
- * screen (for example, to trigger a refresh of data).
- * @param logger Optional [Logger] to log events inside this MVFlow object associated with this view (but not
- * others). If null, a default logger might be used.
- */
- fun takeView(
- viewCoroutineScope: CoroutineScope,
- view: MVIView<State, Action>,
- initialActions: List<Action> = emptyList(),
- logger: Logger? = null
- )
-
- /**
- * This method adds an external source of actions into the MVFlow object.
- *
- * This might be useful if you need to update your state based on things happening outside the [View], such as
- * timers, external database updates, push notifications, etc.
- *
- * @param actions the flow of events. You might want to have a look at
- * [kotlinx.coroutines.flow.callbackFlow].
- * @param logger Optional [Logger] to log events inside this MVFlow object associated with this external Flow (but
- * not others). If null, a default logger might be used.
- */
- fun addExternalActions(
- actions: Flow<Action>,
- logger: Logger? = null
- )
-}
diff --git a/gradle.properties b/gradle.properties
index 896d588..2fb49e8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,4 +17,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
-gitLabPrivateToken="" \ No newline at end of file
+gitLabPrivateToken=""
diff --git a/settings.gradle b/settings.gradle
index dca731c..e39b561 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,2 @@
-include ':flow-mvi'
include ':app'
rootProject.name = "PrivacyCentralApp" \ No newline at end of file