diff options
author | Amit Kumar <amitkma@e.email> | 2021-05-12 12:45:05 +0530 |
---|---|---|
committer | Amit Kumar <amitkma@e.email> | 2021-05-12 12:45:05 +0530 |
commit | 5296470724c8da6826bb3be8ccf684cef96ba119 (patch) | |
tree | da6169b0aeaa31fcb41f387b2aedfbc0fe9dc589 | |
parent | 4e0a3a7967be34a4b2c3ff03ec5b42072d45002d (diff) | |
parent | 3ec3e188e182f2612c07b83b09c37d59c3154da9 (diff) |
Merge branch 'flow-mvi'
-rw-r--r-- | DEVELOPMENT.md | 252 | ||||
-rw-r--r-- | README.md | 127 | ||||
-rw-r--r-- | art/AbstractMVI.png | bin | 0 -> 196890 bytes | |||
-rw-r--r-- | art/CleanArchitecture.jpg | bin | 0 -> 107444 bytes | |||
-rw-r--r-- | art/MVI-Feature.png | bin | 0 -> 24036 bytes | |||
-rw-r--r-- | art/privacycentral-arch.png | bin | 0 -> 77197 bytes | |||
-rw-r--r-- | build.gradle | 2 | ||||
-rw-r--r-- | privapp-permissions-foundation.e.privacycentralapp.xml | 8 | ||||
-rw-r--r-- | spotless/copyright.kt | 2 |
9 files changed, 389 insertions, 2 deletions
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..75e1535 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,252 @@ +# PrivacyCentral Development Guide +This guide contains development related information to help a developer in getting better understanding of project structure. + +## Architecture +The architecture of PrivacyCentralApp is based on [clean architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html). For presentation layer, we use [Model-View-Intent](http://hannesdorfmann.com/android/model-view-intent/) design pattern which is a unidirectional reactive flow pattern. We use it in conjunction to ViewModel to make our features lifecycle aware. Our android app is having single activity multiple fragments. + +### Clean Architecture +Clean architecture is the building block of PrivacyCentralApp. This architecture is based on the following principles: +1. **Independent of Frameworks**. The architecture does not depend on the existence of some library of feature laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints. +2. **Testable**. The business rules can be tested without the UI, Database, Web Server, or any other external element. +3. **Independent of UI**. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules. +4. **Independent of Database**. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database. +5. **Independent of any external agency**. In fact your business rules simply don’t know anything at all about the outside world. + +<img src="art/CleanArchitecture.jpg" width="580" height="425"> + +### Model-View-Intent + +<img src="art/AbstractMVI.png" width="500" height="333"> + +MVI is used at the presentation layer of clean architecture. It is very much similar to [Redux](https://redux.js.org/) in terms of implementation and working. It has three main components. +- **View**: This is where activities, fragments and other android components live. It is responsible for publishing user intent/actions to the model and rendering the state returned by the model. In PrivacyCentralApp, it is just an interface which is implemented by android components. +- **Intent**: In context of our app, we call them actions. These are simple data classes having any extra payload like inputs, ids etc. +- **Model (data layer at presentation level)**: This is responsible for processing the actions, communicating with domain use-cases and mutating the state of the model. It acts as a Store from redux but for our use case, we call it a **Feature**. + +#### Flow-MVI in Privacy Central App +In this app, we have implemented MVI using [Kotlin Flow](https://kotlinlang.org/docs/flow.html). + +<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. + +### Architecture Overview of PrivacyCentral App + +![](art/privacycentral-arch.png) + +Looking at the diagram from right to left: +1. **Database, Preferences and API Helpers**: These are a set of classes with very specific responsibilities for each of them. They can perform some business logic, communicate with the database or make a call to third party API. These classes are implementation of abstract gateways and interfaces (see below). +2. **Abstract Gateways and Interfaces**: An abstraction layer that sits between repositories and framework implementations. The objective of this layer is to keep repositories independent from framework (like databases). +3. **Respositoy Implementation**: These classes provide the implementation of abstract repository defined in the domain. +4. **Repository**: They are the core part of our domain. Each repository is a just an interface whose implementation is provided by the data layer. +5. **Usecase/Interactors/Observers**: The act as the bridge between the presentation layer and data layer. +6. **Entities**: These are business objects for the application. They encapsulate the most general and high-level rules. They are the least likely to change when something external changes. +7. **Features**: MVI specific model (discussed above). +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: +``` +./gradlew test +``` + +To run code style check and formatting tool: +``` +./gradlew spotlessCheck +./gradlew spotlessApply +``` + +## State-of-The-Art +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/README.md b/README.md new file mode 100644 index 0000000..0416190 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# PrivacyCentralApp +An app to let you control and protect your privacy. + +# Features +The following features are currently part of PrivacyCentral app. +1. Centralized overview dashboard. +2. Fake location. +3. Hiding IP address. +4. Manage granular permissions. +5. Control trackers across apps. + +# Technologies +- Kotlin as main language +- Kotlin coroutines and flow for asynchronous code +- AndroidX (core-ktx, fragment-ktx, and lifecycle etc.) +- Google Material Design component for UI elements +- Timber for logging +- Room for database (may use datastore too) +- Hilt for DI +- MapBox for map support +- LeakCanary for keeping an eye on memory leaks. +- Junit for unit testing +- Espresso for integration testing. +- + +# Flavours +This app comes in two flavours, one for /e/OS and second one for other android (where app doesn't have system access). For more information refer to [Architecture Guide](DEVELOPMENT.md) + +# Testing +Need to write test and add code coverage. + +# Development + +## Setup +You can use any latest stable version of android studio to be able to build this app. + +## Supported Versions +- Minimum SDK: 24 (Android N) +- Compile SDK: 30 (Android R) +- Target SDK: 30 (Android R) + +## API Keys +This project uses [Mapbox](https://docs.mapbox.com/android/maps/guides/install/) sdk for displaying maps. To download and use the mapbox sdk, you need to supply API key and secret and set them as follows: + +### For local build +You can set them in local.properties +``` +MAPBOX_KEY=<insert mapbox public key> +MAPBOX_SECRET_KEY=<insert mapbox secret key> +``` +**IMP: Never add this file to version control.** + +### For CI build +When building in CI environment, we don't have local.properties file. So the following environment variables must be set: +``` +export MAPBOX_KEY=<insert mapbox public key> +export MAPBOX_SECRET_KEY=<insert mapbox secret key> +``` + +## Code Style and Quality +This project uses [ktlint](https://github.com/pinterest/ktlint), provided via the [spotless](https://github.com/diffplug/spotless) gradle plugin. To maintain the same code style, all the codestyle configuration has been bundled into the project. + +To check for any codestyle related error, `./gradlew spotlessCheck`. Use `./gradlew spotlessApply` to autoformat in order to fix any code quality related issues. + +### Setting up pre-commit hooks +To strictly enforce the code quality, this project has a pre-commit hook which is triggered everytime before you commit any changes (only applies to *.kt, *.gradle, *.md and *.gitignore). You must setup the pre-commit hook before doing any changes to the project. For that, this project has a script which can executed as follows: +```bash +hooks/pre-commit +``` + +## Build +If you'd like to build PrivacyCentral locally, you should be able to just clone and build with no issues. + +For building from CLI, you can execute this command: +```bash +./gradlew build +``` + +## How to use PrivacyCentral apk +You can build the apk locally by using above instructions or you can download the latest stable apk from `master` branch pipeline. + +### To run apk on /e/OS devices +PrivacyCentral needs to be installed as system app and whitelisting in order to grant some system specific permissions. Follow these steps to make it work properly on /e/OS +1. Sign the apk with platform certificate. You can use this command to do that -> `apksigner sign --key <platform-key> --cert <platform-cert> --out PrivacyCentral.apk <downloaded-apk-path>` +2. Install apk as system app and push permissions whitelist with following commands: +```bash +adb root && adb remount +adb shell mkdir system/priv-app/PrivacyCentral +adb push PrivacyCentral.apk system/priv-app/PrivacyCentral +``` + +3. To push permissions whitelist, you can use the following command. It requires the whitelisting [privapp-permissions-foundation.e.privacycentralapp.xml](privapp-permissions-foundation.e.privacycentralapp.xml) file that can be found in the project repository. +```bash +adb push privapp-permissions-foundation.e.privacycentralapp.xml /system/etc/permissions/ + +# Now reboot the device +adb reboot +``` + +### To run apk on stock android devices +You can simply install the apk. Keep in that mind all features won't be available on stock android devices. + +> Volla!!!, PrivacyCentral is installed successfully in your device. + +# Distribution +This project can be distributed as prebuilt apk with /e/OS or it can be published on other app stores for non /e/OS devices. + +# For developers and contributers +Please refer to [Development Guide](DEVELOPMENT.md) for detailed instructions about project structure, architecture, design patterns and testing guide. + +# License +``` +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/. +``` diff --git a/art/AbstractMVI.png b/art/AbstractMVI.png Binary files differnew file mode 100644 index 0000000..eeca62c --- /dev/null +++ b/art/AbstractMVI.png diff --git a/art/CleanArchitecture.jpg b/art/CleanArchitecture.jpg Binary files differnew file mode 100644 index 0000000..3cd44fb --- /dev/null +++ b/art/CleanArchitecture.jpg diff --git a/art/MVI-Feature.png b/art/MVI-Feature.png Binary files differnew file mode 100644 index 0000000..de85c3d --- /dev/null +++ b/art/MVI-Feature.png diff --git a/art/privacycentral-arch.png b/art/privacycentral-arch.png Binary files differnew file mode 100644 index 0000000..93f26bc --- /dev/null +++ b/art/privacycentral-arch.png diff --git a/build.gradle b/build.gradle index 43c6bc0..492cca5 100644 --- a/build.gradle +++ b/build.gradle @@ -81,7 +81,7 @@ subprojects { target '**/*.kt' targetExclude("$buildDir/**/*.kt") targetExclude('bin/**/*.kt') - + targetExclude '**/spotless/*.kt' ktlint(Versions.ktlint) licenseHeaderFile rootProject.file('spotless/copyright.kt') } diff --git a/privapp-permissions-foundation.e.privacycentralapp.xml b/privapp-permissions-foundation.e.privacycentralapp.xml new file mode 100644 index 0000000..4a0fcfe --- /dev/null +++ b/privapp-permissions-foundation.e.privacycentralapp.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<permissions> + <privapp-permissions package="foundation.e.privacymodulesdemo.e"> + <permission name="android.permission.UPDATE_APP_OPS_STATS"/> + <permission name="android.permission.WATCH_APPOPS"/> + <permission name="android.permission.GET_APP_OPS_STATS"/> + </privapp-permissions> +</permissions>
\ No newline at end of file diff --git a/spotless/copyright.kt b/spotless/copyright.kt index ffd1cad..698a463 100644 --- a/spotless/copyright.kt +++ b/spotless/copyright.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 E FOUNDATION + * Copyright (C) $YEAR 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 |