summaryrefslogtreecommitdiff
path: root/app/src/main/java/foundation/e/privacycentralapp
diff options
context:
space:
mode:
authorRomain Hunault <romain.hunault@e.email>2021-08-13 10:14:03 +0000
committerRomain Hunault <romain.hunault@e.email>2021-08-13 10:14:03 +0000
commitdaea2f9510ac1af22a4e2e2f3db7c2d6d314008b (patch)
tree2dfcd606e8693a79520f9931e566c237547e7cd1 /app/src/main/java/foundation/e/privacycentralapp
parent97b51f18dcc2f87a9cdd7f482033e30a6282d853 (diff)
parentf522c4615503120b5e62e45405e8c6c3b18e5e4a (diff)
Merge branch 'features/blocker' into 'master'
Add blocker feature See merge request e/privacy-central/privacycentralapp!6
Diffstat (limited to 'app/src/main/java/foundation/e/privacycentralapp')
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt3
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt8
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/dummy/TrackersDataSource.kt105
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt29
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt9
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt6
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt6
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsAdapter.kt61
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsFragment.kt101
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersAdapter.kt56
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt142
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt76
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt42
14 files changed, 637 insertions, 9 deletions
diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
index 364ae4a..fcc2eaa 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt
@@ -26,6 +26,7 @@ import foundation.e.privacymodules.location.FakeLocation
import foundation.e.privacymodules.location.IFakeLocation
import foundation.e.privacymodules.permissions.PermissionsPrivacyModule
import foundation.e.privacymodules.permissions.data.ApplicationDescription
+import lineageos.blockers.BlockerInterface
/**
* Simple container to hold application wide dependencies.
@@ -55,4 +56,6 @@ class DependencyContainer constructor(val app: Application) {
val fakeLocationViewModelFactory by lazy {
FakeLocationViewModelFactory(locationApi)
}
+
+ val blockerService = BlockerInterface.getInstance(context)
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt
index 9372a66..153e0c1 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt
@@ -18,9 +18,17 @@
package foundation.e.privacycentralapp
import android.app.Application
+import foundation.e.privacycentralapp.dummy.TrackersDataSource
class PrivacyCentralApplication : Application() {
// Initialize the dependency container.
val dependencyContainer: DependencyContainer by lazy { DependencyContainer(this) }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ // Inject blocker service in trackers source.
+ TrackersDataSource.injectBlockerService(dependencyContainer.blockerService)
+ }
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt
index f156e09..5c18548 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt
@@ -40,4 +40,6 @@ abstract class ToolbarFragment(@LayoutRes contentLayoutId: Int) : Fragment(conte
open fun setupToolbar(toolbar: MaterialToolbar) {
toolbar.title = getTitle()
}
+
+ fun getToolbar(): MaterialToolbar? = view?.findViewById(R.id.toolbar)
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackersDataSource.kt b/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackersDataSource.kt
new file mode 100644
index 0000000..b6f319c
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackersDataSource.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.dummy
+
+import foundation.e.privacycentralapp.R
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import lineageos.blockers.BlockerInterface
+
+data class TrackedApp(val appName: String, val isEnabled: Boolean, val iconId: Int)
+
+data class Tracker(
+ val name: String,
+ val domain: String? = null,
+ val ipAddress: String? = null,
+ val trackedApps: List<TrackedApp>
+)
+
+object TrackersDataSource {
+
+ private lateinit var blockerService: BlockerInterface
+
+ val facebook = TrackedApp("Facebook", true, R.drawable.ic_facebook)
+ val firefox = TrackedApp("Firefox", true, R.drawable.ic_facebook)
+ val google = TrackedApp("Google", true, R.drawable.ic_facebook)
+ val whatsapp = TrackedApp("Whatsapp", true, R.drawable.ic_facebook)
+ val blisslauncher = TrackedApp("BlissLauncher", true, R.drawable.ic_facebook)
+ val youtube = TrackedApp("Youtube", true, R.drawable.ic_facebook)
+
+ val crashlytics = Tracker(
+ "Google Crashlytics",
+ domain = "|0b|crashlytics|03|com",
+ trackedApps = listOf(facebook, firefox)
+ )
+
+ val facebookAds = Tracker(
+ "Facebook Analytics",
+ domain = "|08|facebook|03|com",
+ trackedApps = listOf(facebook, whatsapp)
+ )
+ val rubiconTracker = Tracker(
+ "Rubicon Projects",
+ domain = "|03|ads|0e|rubiconproject|03|com",
+ trackedApps = listOf(google, blisslauncher, youtube)
+ )
+ val googleAnalytics = Tracker(
+ "Google Analytics",
+ domain = "|10|google-analytics|03|com",
+ trackedApps = listOf(facebook, firefox)
+ )
+
+ val _trackers =
+ MutableStateFlow(listOf(crashlytics, facebookAds, rubiconTracker, googleAnalytics))
+ val trackers = _trackers.asStateFlow()
+
+ fun injectBlockerService(blockerInterface: BlockerInterface) {
+ this.blockerService = blockerInterface
+ }
+
+ fun getTracker(name: String): Tracker? {
+ try {
+ return _trackers.value.first {
+ it.name == name
+ }
+ } catch (e: NoSuchElementException) {
+ return null
+ }
+ }
+
+ fun toggleTracker(tracker: Tracker, enable: Boolean): Boolean {
+ val result = if (!enable) {
+ blockerService.blockDomain(tracker.domain)
+ } else {
+ blockerService.unblockDomain(tracker.domain)
+ }
+
+ if (result) {
+ _trackers.value = _trackers.value.map {
+ if (it.name == tracker.name) {
+ it.copy(
+ trackedApps = it.trackedApps.map { app ->
+ app.copy(isEnabled = enable)
+ }
+ )
+ } else it
+ }
+ }
+ return result
+ }
+}
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
index 5a20489..c26fce1 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt
@@ -25,6 +25,7 @@ import foundation.e.flowmvi.feature.BaseFeature
import foundation.e.privacycentralapp.dummy.DummyDataSource
import foundation.e.privacycentralapp.dummy.InternetPrivacyMode
import foundation.e.privacycentralapp.dummy.LocationMode
+import foundation.e.privacycentralapp.dummy.TrackersDataSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
@@ -76,6 +77,7 @@ class DashboardFeature(
object ShowFakeMyLocationAction : Action()
object ShowInternetActivityPrivacyAction : Action()
object ShowAppsPermissions : Action()
+ object ShowTrackers : Action()
}
sealed class Effect {
@@ -92,12 +94,14 @@ class DashboardFeature(
object LoadingDashboardEffect : Effect()
data class UpdateActiveTrackersCountEffect(val count: Int) : Effect()
+ data class UpdateTotalTrackersCountEffect(val count: Int) : Effect()
data class UpdateLocationModeEffect(val mode: LocationMode) : Effect()
data class UpdateInternetActivityModeEffect(val mode: InternetPrivacyMode) : Effect()
data class UpdateAppsUsingLocationPermEffect(val apps: Int) : Effect()
object OpenFakeMyLocationEffect : Effect()
object OpenInternetActivityPrivacyEffect : Effect()
object OpenAppsPermissionsEffect : Effect()
+ object OpenTrackersEffect : Effect()
}
companion object {
@@ -127,6 +131,11 @@ class DashboardFeature(
state.copy(activeTrackersCount = effect.count)
} else state
}
+ is Effect.UpdateTotalTrackersCountEffect -> {
+ if (state is State.DashboardState) {
+ state.copy(trackersCount = effect.count)
+ } else state
+ }
is Effect.UpdateInternetActivityModeEffect -> {
if (state is State.DashboardState) {
state.copy(internetPrivacyMode = effect.mode)
@@ -144,14 +153,27 @@ class DashboardFeature(
Effect.OpenFakeMyLocationEffect -> state
Effect.OpenAppsPermissionsEffect -> state
Effect.OpenInternetActivityPrivacyEffect -> state
+ Effect.OpenTrackersEffect -> state
}
},
actor = { _: State, action: Action ->
Log.d("Feature", "action: $action")
when (action) {
Action.ObserveDashboardAction -> merge(
- DummyDataSource.activeTrackersCount.map {
- Effect.UpdateActiveTrackersCountEffect(it)
+ TrackersDataSource.trackers.map {
+ var activeTrackersCount: Int = 0
+ outer@ for (tracker in it) {
+ for (app in tracker.trackedApps) {
+ if (!app.isEnabled) {
+ continue@outer
+ }
+ }
+ activeTrackersCount++
+ }
+ Effect.UpdateActiveTrackersCountEffect(activeTrackersCount)
+ },
+ TrackersDataSource.trackers.map {
+ Effect.UpdateTotalTrackersCountEffect(it.size)
},
DummyDataSource.appsUsingLocationPerm.map {
Effect.UpdateAppsUsingLocationPermEffect(it.size)
@@ -185,6 +207,7 @@ class DashboardFeature(
Action.ShowInternetActivityPrivacyAction -> flowOf(
Effect.OpenInternetActivityPrivacyEffect
)
+ Action.ShowTrackers -> flowOf(Effect.OpenTrackersEffect)
}
},
singleEventProducer = { state, _, effect ->
@@ -197,6 +220,8 @@ class DashboardFeature(
SingleEvent.NavigateToInternetActivityPrivacySingleEvent
else if (state is State.DashboardState && effect is Effect.OpenAppsPermissionsEffect)
SingleEvent.NavigateToPermissionsSingleEvent
+ else if (state is State.DashboardState && effect is Effect.OpenTrackersEffect)
+ SingleEvent.NavigateToTrackersSingleEvent
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 c57e6cc..e7ce353 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
@@ -38,6 +38,7 @@ import foundation.e.privacycentralapp.dummy.mapToString
import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyFragment
import foundation.e.privacycentralapp.features.location.FakeLocationFragment
import foundation.e.privacycentralapp.features.permissions.PermissionsFragment
+import foundation.e.privacycentralapp.features.trackers.TrackersFragment
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
@@ -84,6 +85,11 @@ class DashboardFragment :
}
}
DashboardFeature.SingleEvent.NavigateToTrackersSingleEvent -> {
+ requireActivity().supportFragmentManager.commit {
+ add<TrackersFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
}
}
}
@@ -110,6 +116,9 @@ class DashboardFragment :
it.findViewById<RelativeLayout>(R.id.apps_permissions).setOnClickListener {
viewModel.submitAction(DashboardFeature.Action.ShowAppsPermissions)
}
+ it.findViewById<RelativeLayout>(R.id.am_i_tracked).setOnClickListener {
+ viewModel.submitAction(DashboardFeature.Action.ShowTrackers)
+ }
}
}
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
index 7e45049..9124f85 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt
@@ -36,8 +36,7 @@ class FakeLocationFeature(
coroutineScope: CoroutineScope,
reducer: Reducer<State, Effect>,
actor: Actor<State, Action, Effect>,
- singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>,
- private val locationApi: LocationApiDelegate
+ singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
) : BaseFeature<FakeLocationFeature.State, FakeLocationFeature.Action, FakeLocationFeature.Effect, FakeLocationFeature.SingleEvent>(
initialState,
actor,
@@ -206,8 +205,7 @@ class FakeLocationFeature(
is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
else -> null
}
- },
- locationApi = locationApi
+ }
)
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt
index 4f9b602..4905dca 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt
@@ -36,17 +36,17 @@ class PermissionAppsAdapter(
val appName: TextView = view.findViewById(R.id.app_title)
@SuppressLint("UseSwitchCompatOrMaterialCode")
- val togglePermission: Switch = view.findViewById(R.id.togglePermission)
+ val togglePermission: Switch = view.findViewById(R.id.toggle)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PermissionViewHolder {
val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.item_permission_apps, parent, false)
+ .inflate(R.layout.item_app_toggle, parent, false)
val holder = PermissionViewHolder(view)
holder.togglePermission.setOnCheckedChangeListener { _, isChecked ->
listener(dataSet[holder.adapterPosition].first, isChecked)
}
- view.findViewById<Switch>(R.id.togglePermission)
+ view.findViewById<Switch>(R.id.toggle)
return holder
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsAdapter.kt
new file mode 100644
index 0000000..ae236b9
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsAdapter.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Switch
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.privacycentralapp.R
+import foundation.e.privacycentralapp.dummy.Tracker
+
+class TrackerAppsAdapter(
+ private var tracker: Tracker,
+ private val listener: (Tracker, Boolean) -> Unit
+) :
+ RecyclerView.Adapter<TrackerAppsAdapter.TrackerViewHolder>() {
+
+ class TrackerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val titleView: TextView = view.findViewById(R.id.app_title)
+ @SuppressLint("UseSwitchCompatOrMaterialCode")
+ val toggleBlocker: Switch = view.findViewById(R.id.toggle)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackerViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_app_toggle, parent, false)
+ val holder = TrackerViewHolder(view)
+ holder.toggleBlocker.setOnClickListener {
+ if (it is Switch) {
+ listener(tracker, it.isChecked)
+ }
+ }
+ return holder
+ }
+
+ override fun onBindViewHolder(holder: TrackerViewHolder, position: Int) {
+ val app = tracker.trackedApps[position]
+ holder.titleView.text = app.appName
+ holder.toggleBlocker.isChecked = app.isEnabled
+ }
+
+ override fun getItemCount(): Int = tracker.trackedApps.size
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsFragment.kt
new file mode 100644
index 0000000..fff24dc
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsFragment.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.os.Bundle
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.flowmvi.MVIView
+import foundation.e.privacycentralapp.R
+import foundation.e.privacycentralapp.common.NavToolbarFragment
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+
+class TrackerAppsFragment :
+ NavToolbarFragment(R.layout.fragment_tracker_apps),
+ MVIView<TrackersFeature.State, TrackersFeature.Action> {
+
+ private val viewModel: TrackersViewModel by viewModels()
+
+ private val TAG = "TrackerAppsFragment"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchWhenStarted {
+ viewModel.trackersFeature.takeView(this, this@TrackerAppsFragment)
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.trackersFeature.singleEvents.collect { event ->
+ when (event) {
+ is TrackersFeature.SingleEvent.ErrorEvent -> displayToast(event.error)
+ is TrackersFeature.SingleEvent.BlockerErrorEvent -> {
+ displayToast("Couldn't toggle")
+ // Re-render the current state to reset the switches.
+ render(viewModel.trackersFeature.state.value)
+ }
+ }
+ }
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.submitAction(
+ TrackersFeature.Action.ObserveTracker(
+ requireArguments().getString(
+ "TRACKER"
+ )
+ )
+ )
+ }
+ }
+
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
+ }
+
+ override fun getTitle(): String = getString(R.string.tracker)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ view.findViewById<RecyclerView>(R.id.recylcer_view_tracker_apps)?.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ setHasFixedSize(true)
+ }
+ }
+
+ override fun render(state: TrackersFeature.State) {
+ Log.d(TAG, "render() called with: state = $state")
+ state.currentSelectedTracker?.let { tracker ->
+ view?.findViewById<RecyclerView>(R.id.recylcer_view_tracker_apps)?.adapter = TrackerAppsAdapter(tracker) { it, grant ->
+ viewModel.submitAction(
+ TrackersFeature.Action.ToggleTrackerAction(
+ it,
+ grant
+ )
+ )
+ }
+ getToolbar()?.title = tracker.name
+ }
+ }
+
+ override fun actions(): Flow<TrackersFeature.Action> = viewModel.actions
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersAdapter.kt
new file mode 100644
index 0000000..cef069e
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersAdapter.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.privacycentralapp.R
+import foundation.e.privacycentralapp.dummy.Tracker
+
+class TrackersAdapter(
+ private var dataSet: List<Tracker> = emptyList(),
+ private val listener: (Tracker) -> Unit
+) :
+ RecyclerView.Adapter<TrackersAdapter.TrackerViewHolder>() {
+
+ class TrackerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val titleView: TextView = view as TextView
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackerViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_list_tracker, parent, false)
+ val holder = TrackerViewHolder(view)
+ holder.titleView.setOnClickListener { listener(dataSet[holder.adapterPosition]) }
+ return holder
+ }
+
+ override fun onBindViewHolder(holder: TrackerViewHolder, position: Int) {
+ val tracker = dataSet[position]
+ holder.titleView.text = tracker.name
+ }
+
+ override fun getItemCount(): Int = dataSet.size
+
+ fun setData(data: List<Tracker>) {
+ this.dataSet = data
+ }
+}
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
new file mode 100644
index 0000000..9400181
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.dummy.Tracker
+import foundation.e.privacycentralapp.dummy.TrackersDataSource
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+// 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 trackers: List<Tracker> = emptyList(),
+ val currentSelectedTracker: Tracker? = null
+ )
+
+ sealed class SingleEvent {
+ data class ErrorEvent(val error: String) : SingleEvent()
+ object BlockerErrorEvent : SingleEvent()
+ }
+
+ sealed class Action {
+ object ObserveTrackers : Action()
+ data class SetSelectedTracker(val tracker: Tracker) : Action()
+ data class ToggleTrackerAction(
+ val tracker: Tracker,
+ val grant: Boolean
+ ) : Action()
+ data class ObserveTracker(val tracker: String?) : Action()
+ }
+
+ sealed class Effect {
+ data class TrackersLoadedEffect(val trackers: List<Tracker>) : Effect()
+ data class TrackerSelectedEffect(val tracker: Tracker) : Effect()
+ data class TrackerToggleEffect(val result: Boolean) : Effect()
+ data class ErrorEffect(val message: String) : Effect()
+ data class TrackerLoadedEffect(val tracker: Tracker) : Effect()
+ }
+
+ companion object {
+ fun create(
+ initialState: State = State(),
+ coroutineScope: CoroutineScope
+ ) = TrackersFeature(
+ initialState, coroutineScope,
+ reducer = { state, effect ->
+ when (effect) {
+ is Effect.TrackersLoadedEffect -> State(effect.trackers)
+ is Effect.TrackerSelectedEffect -> state.copy(currentSelectedTracker = effect.tracker)
+ is Effect.ErrorEffect -> state
+ is Effect.TrackerToggleEffect -> {
+ state
+ }
+ is Effect.TrackerLoadedEffect -> {
+ state.copy(currentSelectedTracker = effect.tracker)
+ }
+ }
+ },
+ actor = { state, action ->
+ when (action) {
+ Action.ObserveTrackers -> TrackersDataSource.trackers.map {
+ Effect.TrackersLoadedEffect(
+ it
+ )
+ }
+ is Action.SetSelectedTracker -> flowOf(
+ Effect.TrackerSelectedEffect(
+ action.tracker
+ )
+ )
+
+ is Action.ToggleTrackerAction -> {
+ if (state.currentSelectedTracker != null) {
+ val result = TrackersDataSource.toggleTracker(
+ action.tracker,
+ action.grant
+ )
+ flowOf(Effect.TrackerToggleEffect(result))
+ } else {
+ flowOf(Effect.ErrorEffect("Can't toggle tracker"))
+ }
+ }
+ is Action.ObserveTracker -> {
+ if (action.tracker == null) {
+ flowOf(Effect.ErrorEffect("Null tracker id passed"))
+ } else {
+ val tracker = TrackersDataSource.getTracker(action.tracker)
+ if (tracker != null) {
+ flowOf(Effect.TrackerLoadedEffect(tracker))
+ } else {
+ flowOf(Effect.ErrorEffect("Can't find tracker with name ${action.tracker}"))
+ }
+ }
+ }
+ }
+ },
+ singleEventProducer = { _, _, effect ->
+ when (effect) {
+ is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
+ is Effect.TrackerToggleEffect -> {
+ if (!effect.result) SingleEvent.BlockerErrorEvent else null
+ }
+ 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
new file mode 100644
index 0000000..e3dc941
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.privacycentralapp.features.trackers
+
+import android.os.Bundle
+import android.view.View
+import androidx.core.os.bundleOf
+import androidx.fragment.app.add
+import androidx.fragment.app.commit
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.flowmvi.MVIView
+import foundation.e.privacycentralapp.R
+import foundation.e.privacycentralapp.common.NavToolbarFragment
+import kotlinx.coroutines.flow.Flow
+
+class TrackersFragment :
+ NavToolbarFragment(R.layout.fragment_trackers),
+ MVIView<TrackersFeature.State, TrackersFeature.Action> {
+
+ private val viewModel: TrackersViewModel by viewModels()
+ private lateinit var trackersAdapter: TrackersAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchWhenStarted {
+ viewModel.trackersFeature.takeView(this, this@TrackersFragment)
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.submitAction(TrackersFeature.Action.ObserveTrackers)
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ trackersAdapter = TrackersAdapter {
+ requireActivity().supportFragmentManager.commit {
+ val bundle = bundleOf("TRACKER" to it.name)
+ add<TrackerAppsFragment>(R.id.container, args = bundle)
+ setReorderingAllowed(true)
+ addToBackStack("trackers")
+ }
+ // viewModel.submitAction(TrackersFeature.Action.SetSelectedTracker(it))
+ }
+ view.findViewById<RecyclerView>(R.id.recylcer_view_trackers)?.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ setHasFixedSize(true)
+ adapter = trackersAdapter
+ }
+ }
+
+ override fun getTitle() = getString(R.string.trackers)
+
+ override fun render(state: TrackersFeature.State) {
+ trackersAdapter.setData(state.trackers)
+ }
+
+ override fun actions(): Flow<TrackersFeature.Action> = viewModel.actions
+}
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
new file mode 100644
index 0000000..ee89887
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt
@@ -0,0 +1,42 @@
+/*
+ * 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 androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+
+class TrackersViewModel : ViewModel() {
+
+ private val _actions = MutableSharedFlow<TrackersFeature.Action>()
+ val actions = _actions.asSharedFlow()
+
+ val trackersFeature: TrackersFeature by lazy {
+ TrackersFeature.create(coroutineScope = viewModelScope)
+ }
+
+ fun submitAction(action: TrackersFeature.Action) {
+ Log.d("TrackersViewModel", "submitting action")
+ viewModelScope.launch {
+ _actions.emit(action)
+ }
+ }
+}