diff options
author | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2022-07-28 06:04:25 +0000 |
---|---|---|
committer | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2022-07-28 06:04:25 +0000 |
commit | 12510a55c9c2b1d21c6e1f45d0058778ddfc9eaa (patch) | |
tree | f87e29f670323b7173e5e3875112271c8835a5d3 | |
parent | 3ca73e64ddd25c7c20eca2e4e0db77032db848c0 (diff) | |
parent | b4d35c1c12120503e74d7ae99edd94302673acf6 (diff) | |
download | advanced-privacy-12510a55c9c2b1d21c6e1f45d0058778ddfc9eaa.tar.gz advanced-privacy-12510a55c9c2b1d21c6e1f45d0058778ddfc9eaa.tar.bz2 advanced-privacy-12510a55c9c2b1d21c6e1f45d0058778ddfc9eaa.zip |
Merge branch 'remove_flow_mvi' into 'main'
#5444 Fix CPU consumption - remove flow-mvi dependency
See merge request e/os/advanced-privacy!74
49 files changed, 1089 insertions, 2054 deletions
@@ -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 |