summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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