From f4fe8a4d881deef7e43ffe296df804c8c2c7a657 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 16 Jun 2021 00:33:46 +0530 Subject: Add tracker UI and integrate lineage blocker service --- app/build.gradle | 2 +- app/libs/classes.jar | Bin 429238 -> 0 bytes app/libs/lineage-sdk.jar | Bin 0 -> 430252 bytes .../e/privacycentralapp/DependencyContainer.kt | 3 + .../privacycentralapp/PrivacyCentralApplication.kt | 8 ++ .../e/privacycentralapp/common/ToolbarFragment.kt | 2 + .../e/privacycentralapp/dummy/DummyDataSource.kt | 1 + .../privacycentralapp/dummy/TrackersDataSource.kt | 93 ++++++++++++++++ .../features/dashboard/DashboardFeature.kt | 23 +++- .../features/location/FakeLocationFeature.kt | 6 +- .../features/permissions/PermissionAppsAdapter.kt | 6 +- .../features/trackers/TrackerAppsAdapter.kt | 59 ++++++++++ .../features/trackers/TrackerAppsFragment.kt | 78 +++++++++++++ .../features/trackers/TrackersAdapter.kt | 56 ++++++++++ .../features/trackers/TrackersFeature.kt | 121 +++++++++++++++++++++ .../features/trackers/TrackersFragment.kt | 63 +++++++---- .../features/trackers/TrackersViewModel.kt | 40 +++++++ app/src/main/res/drawable/dummy_trackers_usage.png | Bin 0 -> 16635 bytes .../main/res/layout/fragment_permission_apps.xml | 2 +- app/src/main/res/layout/fragment_tracker_apps.xml | 38 +++++++ app/src/main/res/layout/fragment_trackers.xml | 37 ++++++- app/src/main/res/layout/item_app_toggle.xml | 44 ++++++++ app/src/main/res/layout/item_list_tracker.xml | 29 +++++ app/src/main/res/layout/item_permission_apps.xml | 44 -------- app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 5 + scripts/sign_and_push.sh | 2 +- 27 files changed, 682 insertions(+), 82 deletions(-) delete mode 100644 app/libs/classes.jar create mode 100644 app/libs/lineage-sdk.jar create mode 100644 app/src/main/java/foundation/e/privacycentralapp/dummy/TrackersDataSource.kt create mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsAdapter.kt create mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsFragment.kt create mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersAdapter.kt create mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt create mode 100644 app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt create mode 100644 app/src/main/res/drawable/dummy_trackers_usage.png create mode 100644 app/src/main/res/layout/fragment_tracker_apps.xml create mode 100644 app/src/main/res/layout/item_app_toggle.xml create mode 100644 app/src/main/res/layout/item_list_tracker.xml delete mode 100644 app/src/main/res/layout/item_permission_apps.xml diff --git a/app/build.gradle b/app/build.gradle index fe9e4d7..8c18e06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,7 +84,7 @@ android { dependencies { compileOnly files('libs/e-ui-sdk-1.0.1-q.jar') - implementation files('libs/classes.jar') + implementation files('libs/lineage-sdk.jar') implementation project(":privacymodulesapi") // include the google specific version of the modules, just for the google flavor diff --git a/app/libs/classes.jar b/app/libs/classes.jar deleted file mode 100644 index 74032a0..0000000 Binary files a/app/libs/classes.jar and /dev/null differ diff --git a/app/libs/lineage-sdk.jar b/app/libs/lineage-sdk.jar new file mode 100644 index 0000000..c2f04a7 Binary files /dev/null and b/app/libs/lineage-sdk.jar differ 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/DummyDataSource.kt b/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt index fe61354..dd6112d 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt @@ -227,4 +227,5 @@ object DummyDataSource { _appsUsingLocationPerm.value = _populatedPermissions.value[permissionId].packagesAllowed } } + } 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..9485a26 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackersDataSource.kt @@ -0,0 +1,93 @@ +/* + * 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 . + */ + +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 +) + +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 (Demo)", + domain = "google.com", + trackedApps = listOf(facebook, firefox) + ) + + val facebookAds = Tracker( + "Facebook (Demo)", + domain = "google.com", + trackedApps = listOf(facebook, whatsapp) + ) + val rubiconTracker = Tracker( + "Rubicon Projects", + domain = "google.com", + trackedApps = listOf(google, blisslauncher, youtube) + ) + val googleAnalytics = Tracker( + "Google Analytics", + domain = "google.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 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 + } +} \ No newline at end of file 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 dc02c91..a273b88 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 @@ -93,6 +94,7 @@ 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() @@ -129,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) @@ -153,8 +160,20 @@ class DashboardFeature( 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) 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, actor: Actor, - singleEventProducer: SingleEventProducer, - private val locationApi: LocationApiDelegate + singleEventProducer: SingleEventProducer ) : BaseFeature( 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(R.id.togglePermission) + view.findViewById(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..04e3f04 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsAdapter.kt @@ -0,0 +1,59 @@ +/* + * 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 . + */ + +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() { + + 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.setOnCheckedChangeListener { _, isChecked -> + listener(tracker, 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..67ae0cc --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsFragment.kt @@ -0,0 +1,78 @@ +/* + * 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 . + */ + +package foundation.e.privacycentralapp.features.trackers + +import android.os.Bundle +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 { + + private val viewModel: TrackersViewModel by viewModels() + + 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) + } + } + } + } + + private fun displayToast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT) + .show() + } + + override fun getTitle(): String = getString(R.string.tracker) + + override fun render(state: TrackersFeature.State) { + state.currentSelectedTracker?.let { tracker -> + view?.findViewById(R.id.recylcer_view_tracker_apps)?.apply { + layoutManager = LinearLayoutManager(requireContext()) + setHasFixedSize(true) + adapter = TrackerAppsAdapter(tracker) { tracker, grant -> + viewModel.submitAction( + TrackersFeature.Action.ToggleTrackerAction( + tracker, + grant + ) + ) + } + } + getToolbar()?.title = tracker.name + } + } + + override fun actions(): Flow = 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 . + */ + +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 = emptyList(), + private val listener: (Tracker) -> Unit +) : + RecyclerView.Adapter() { + + 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) { + 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..f9fbf63 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt @@ -0,0 +1,121 @@ +/* + * 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 . + */ + +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, + actor: Actor, + singleEventProducer: SingleEventProducer +) : BaseFeature( + initialState, + actor, + reducer, + coroutineScope, + { message -> Log.d("PermissionsFeature", message) }, + singleEventProducer +) { + data class State( + val trackers: List = 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() + } + + sealed class Effect { + data class TrackersLoadedEffect(val trackers: List) : Effect() + data class TrackerSelectedEffect(val tracker: Tracker) : Effect() + data class TrackerToggleEffect(val result: Boolean) : Effect() + data class ErrorEffect(val message: String) : 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 + } + }, + 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( + state.currentSelectedTracker, + action.grant + ) + flowOf(Effect.TrackerToggleEffect(result)) + } else { + flowOf(Effect.ErrorEffect("Can't toggle tracker")) + } + } + } + }, + singleEventProducer = { _, _, effect -> + when (effect) { + is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) + is Effect.TrackerToggleEffect -> SingleEvent.BlockerErrorEvent + else -> null + } + } + ) + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt index d4085b4..d0242d3 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt @@ -17,42 +17,63 @@ package foundation.e.privacycentralapp.features.trackers -import android.content.Context import android.os.Bundle import android.view.View -import android.widget.Button -import android.widget.Toast +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 lineageos.blockers.BlockerInterface +import kotlinx.coroutines.flow.Flow class TrackersFragment : - NavToolbarFragment(R.layout.fragment_trackers) { + NavToolbarFragment(R.layout.fragment_trackers), + MVIView { - private lateinit var blockerService: BlockerInterface + private val viewModel: TrackersViewModel by viewModels() + private lateinit var trackersAdapter: TrackersAdapter - override fun onAttach(context: Context) { - super.onAttach(context) - blockerService = BlockerInterface.getInstance(context) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launchWhenStarted { + viewModel.trackersFeature.takeView(this, this@TrackersFragment) + } + lifecycleScope.launchWhenStarted { + viewModel.submitAction(TrackersFeature.Action.ObserveTrackers) + } } - private fun displayToast(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT) - .show() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + trackersAdapter = TrackersAdapter { + viewModel.submitAction(TrackersFeature.Action.SetSelectedTracker(it)) + } + view.findViewById(R.id.recylcer_view_trackers)?.apply { + layoutManager = LinearLayoutManager(requireContext()) + setHasFixedSize(true) + adapter = trackersAdapter + } } override fun getTitle(): String { - return "Trackers" + return getString(R.string.trackers) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - bindClickListeners(view) - } - - private fun bindClickListeners(view: View) { - view.findViewById