diff options
author | jacquarg <guillaume.jacquart@hoodbrains.com> | 2021-11-05 11:20:01 +0100 |
---|---|---|
committer | jacquarg <guillaume.jacquart@hoodbrains.com> | 2021-11-05 11:20:01 +0100 |
commit | 2d210ca863561ac68445e588d1405d9847716347 (patch) | |
tree | 2601b2a04c391d704c2473e629030b3c4c730636 /app/src/main/java | |
parent | a484bf584f4163c8a0a1260e81d598fdec87ff3b (diff) |
Embed trackerfilter aar, ui fixes.
Diffstat (limited to 'app/src/main/java')
13 files changed, 592 insertions, 193 deletions
diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt index 1ba235b..f36405d 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt @@ -24,6 +24,7 @@ import foundation.e.privacycentralapp.data.repositories.LocalStateRepository import foundation.e.privacycentralapp.domain.usecases.AppListUseCase import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase 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.TrackTrackersPrivacyMock import foundation.e.privacycentralapp.features.dashboard.DashBoardViewModelFactory @@ -31,12 +32,15 @@ import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyVi import foundation.e.privacycentralapp.features.location.FakeLocationViewModelFactory import foundation.e.privacycentralapp.features.location.LocationApiDelegate import foundation.e.privacycentralapp.features.trackers.TrackersViewModelFactory +import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersViewModelFactory import foundation.e.privacymodules.ipscrambler.IpScramblerModule import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule 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 foundation.e.trackerfilter.api.BlockTrackersPrivacyModule +import foundation.e.trackerfilter.api.TrackTrackersPrivacyModule import kotlinx.coroutines.GlobalScope import lineageos.blockers.BlockerInterface @@ -67,9 +71,13 @@ class DependencyContainer constructor(val app: Application) { LocationApiDelegate(fakeLocationModule, permissionsModule, appDesc) } + private val blockTrackersPrivacyModule by lazy { BlockTrackersPrivacyModule.getInstance(context) } + // Repositories private val localStateRepository by lazy { LocalStateRepository(context) } - private val trackTrackersPrivacyModule by lazy { TrackTrackersPrivacyMock() } + private val trackTrackersPrivacyModule by lazy { TrackTrackersPrivacyModule.getInstance(context) } + + private val trackersPrivacyMock by lazy { TrackTrackersPrivacyMock() } // Usecases private val getQuickPrivacyStateUseCase by lazy { GetQuickPrivacyStateUseCase(localStateRepository) @@ -81,7 +89,11 @@ class DependencyContainer constructor(val app: Application) { AppListUseCase(permissionsModule) } private val trackersStatisticsUseCase by lazy { - TrackersStatisticsUseCase(trackTrackersPrivacyModule) + TrackersStatisticsUseCase(trackersPrivacyMock) + } + + private val trackersStateUseCase by lazy { + TrackersStateUseCase(trackersPrivacyMock, trackersPrivacyMock, permissionsModule) } // ViewModelFactories @@ -102,4 +114,8 @@ class DependencyContainer constructor(val app: Application) { val trackersViewModelFactory by lazy { TrackersViewModelFactory(getQuickPrivacyStateUseCase, trackersStatisticsUseCase, appListUseCase) } + + val appTrackersViewModelFactory by lazy { + AppTrackersViewModelFactory(trackersStateUseCase) + } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt new file mode 100644 index 0000000..cabe6a1 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt @@ -0,0 +1,54 @@ +/* + * 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.domain.usecases + +import foundation.e.privacymodules.permissions.PermissionsPrivacyModule +import foundation.e.privacymodules.permissions.data.ApplicationDescription +import foundation.e.privacymodules.trackers.IBlockTrackersPrivacyModule +import foundation.e.privacymodules.trackers.ITrackTrackersPrivacyModule +import foundation.e.privacymodules.trackers.Tracker + +class TrackersStateUseCase( + private val blockTrackersPrivacyModule: IBlockTrackersPrivacyModule, + private val trackersPrivacyModule: ITrackTrackersPrivacyModule, + private val permissionsPrivacyModule: PermissionsPrivacyModule +) { + fun getApplicationPermission(packageName: String): ApplicationDescription { + return permissionsPrivacyModule.getApplicationDescription(packageName) + } + + fun getTrackers(appUid: Int): List<Tracker> { + return trackersPrivacyModule.getTrackersForApp(appUid) + } + + fun isWhitelisted(appUid: Int): Boolean { + return blockTrackersPrivacyModule.isWhitelisted(appUid) + } + + fun getTrackersWhitelistIds(appUid: Int): List<Int> { + return blockTrackersPrivacyModule.getWhiteList(appUid).map { it.id } + } + + fun toggleAppWhitelist(appUid: Int, isWhitelisted: Boolean) { + blockTrackersPrivacyModule.setWhiteListed(appUid, isWhitelisted) + } + + fun blockTracker(appUid: Int, tracker: Tracker, isBlocked: Boolean) { + blockTrackersPrivacyModule.setWhiteListed(tracker, appUid, !isBlocked) + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt index 33c3f64..dc0b92b 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt @@ -17,19 +17,19 @@ package foundation.e.privacycentralapp.domain.usecases -import foundation.e.privacycentralapp.dummy.TrackTrackersPrivacyMock +import foundation.e.privacymodules.trackers.ITrackTrackersPrivacyModule class TrackersStatisticsUseCase( - private val trackTrackersPrivacyModule: TrackTrackersPrivacyMock + private val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule ) { - fun getPast24HoursTrackersCalls(): List<Int> { - return trackTrackersPrivacyModule.getPast24HoursTrackersCalls() + fun getPastDayTrackersCalls(): List<Int> { + return trackTrackersPrivacyModule.getPastDayTrackersCalls() } fun getDayMonthYearStatistics(): Triple<List<Int>, List<Int>, List<Int>> { return Triple( - trackTrackersPrivacyModule.getPast24HoursTrackersCalls(), + trackTrackersPrivacyModule.getPastDayTrackersCalls(), trackTrackersPrivacyModule.getPastMonthTrackersCalls(), trackTrackersPrivacyModule.getPastYearTrackersCalls() ) @@ -37,7 +37,7 @@ class TrackersStatisticsUseCase( fun getDayMonthYearCounts(): Triple<Int, Int, Int> { return Triple( - trackTrackersPrivacyModule.getPast24HoursTrackersCount(), + trackTrackersPrivacyModule.getPastDayTrackersCount(), trackTrackersPrivacyModule.getPastMonthTrackersCount(), trackTrackersPrivacyModule.getPastYearTrackersCount() ) diff --git a/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackTrackersPrivacyMock.kt b/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackTrackersPrivacyMock.kt index 55ca6ec..5b1eb9e 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackTrackersPrivacyMock.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackTrackersPrivacyMock.kt @@ -17,11 +17,20 @@ package foundation.e.privacycentralapp.dummy +import foundation.e.privacymodules.trackers.IBlockTrackersPrivacyModule import foundation.e.privacymodules.trackers.ITrackTrackersPrivacyModule import foundation.e.privacymodules.trackers.Tracker -class TrackTrackersPrivacyMock : ITrackTrackersPrivacyModule { - override fun getPast24HoursTrackersCalls(): List<Int> { +class TrackTrackersPrivacyMock : + ITrackTrackersPrivacyModule, + IBlockTrackersPrivacyModule { + + private val trackers = listOf( + Tracker(1, "Crashlytics", null), + Tracker(2, label = "Facebook", null) + ) + + override fun getPastDayTrackersCalls(): List<Int> { return listOf( 2000, 2300, 130, 2500, 1000, 2000, 2000, 2300, 130, 2500, 1000, 2000, @@ -30,7 +39,7 @@ class TrackTrackersPrivacyMock : ITrackTrackersPrivacyModule { ) } - override fun getPast24HoursTrackersCount(): Int { + override fun getPastDayTrackersCount(): Int { return 30 } @@ -64,9 +73,67 @@ class TrackTrackersPrivacyMock : ITrackTrackersPrivacyModule { } override fun getTrackersForApp(appUid: Int): List<Tracker> { - return listOf( - Tracker("Crashlytics", null), - Tracker(label = "Facebook", null) - ) + return trackers + } + + private var isBlockingEnabled = false + private val appWhitelist = mutableSetOf<Int>() + private val trackersWhitelist = mutableMapOf<Int, MutableSet<Tracker>>() + + override fun addListener(listener: IBlockTrackersPrivacyModule.Listener) { + TODO("Not yet implemented") + } + + override fun removeListener(listener: IBlockTrackersPrivacyModule.Listener) { + TODO("Not yet implemented") + } + + override fun clearListeners() { + TODO("Not yet implemented") + } + + override fun disableBlocking() {} + + override fun enableBlocking() {} + + override fun getWhiteList(appUid: Int): List<Tracker> { + return trackersWhitelist[appUid]?.toList() ?: emptyList() + } + + override fun getWhiteListedApp(): List<Int> { + return appWhitelist.toList() + } + + override fun isBlockingEnabled(): Boolean { + return isBlockingEnabled + } + + override fun isWhiteListEmpty(): Boolean { + return appWhitelist.isEmpty() && + (trackersWhitelist.isEmpty() || trackersWhitelist.values.all { it.isEmpty() }) + } + + override fun isWhitelisted(appUid: Int): Boolean { + return appUid in appWhitelist + } + + override fun setWhiteListed(tracker: Tracker, appUid: Int, isWhiteListed: Boolean) { + if (appUid !in trackersWhitelist) { + trackersWhitelist[appUid] = mutableSetOf<Tracker>() + } + + if (isWhiteListed) { + trackersWhitelist[appUid]?.add(tracker) + } else { + trackersWhitelist[appUid]?.remove(tracker) + } + } + + override fun setWhiteListed(appUid: Int, isWhiteListed: Boolean) { + if (isWhiteListed) { + appWhitelist.add(appUid) + } else { + appWhitelist.remove(appUid) + } } } 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 d38d4f6..a6ac87a 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 @@ -190,7 +190,7 @@ class DashboardFeature( Effect.IpScramblingModeUpdatedEffect(it) }, flow { - emit(Effect.TrackersStatisticsUpdatedEffect(trackersStatisticsUseCase.getPast24HoursTrackersCalls())) + emit(Effect.TrackersStatisticsUpdatedEffect(trackersStatisticsUseCase.getPastDayTrackersCalls())) } ) /* 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 deleted file mode 100644 index ae236b9..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsAdapter.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 deleted file mode 100644 index fff24dc..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackerAppsFragment.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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/TrackersFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt index 0394abb..0120fae 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt @@ -64,7 +64,7 @@ class TrackersFeature( sealed class SingleEvent { data class ErrorEvent(val error: String) : SingleEvent() - data class OpenAppDetailsEvent(val packageName: String) : SingleEvent() + data class OpenAppDetailsEvent(val appDesc: ApplicationDescription) : SingleEvent() object BlockerErrorEvent : SingleEvent() } @@ -93,7 +93,7 @@ class TrackersFeature( data class AvailableAppsListEffect( val apps: List<ApplicationDescription> ) : Effect() - data class OpenAppDetailsEffect(val packageName: String) : Effect() + data class OpenAppDetailsEffect(val appDesc: ApplicationDescription) : Effect() object QuickPrivacyDisabledWarningEffect : Effect() data class TrackersLoadedEffect(val trackers: List<Tracker>) : Effect() data class TrackerSelectedEffect(val tracker: Tracker) : Effect() @@ -159,9 +159,11 @@ class TrackersFeature( ) is Action.ClickAppAction -> flowOf( - if (getPrivacyStateUseCase.isQuickPrivacyEnabled) - Effect.OpenAppDetailsEffect(action.packageName) - else Effect.QuickPrivacyDisabledWarningEffect + if (getPrivacyStateUseCase.isQuickPrivacyEnabled) { + state.apps?.find { it.packageName == action.packageName }?.let { + Effect.OpenAppDetailsEffect(it) + } ?: run { Effect.ErrorEffect("Can't find back app.") } + } else Effect.QuickPrivacyDisabledWarningEffect ) Action.ObserveTrackers -> TrackersDataSource.trackers.map { Effect.TrackersLoadedEffect( @@ -202,7 +204,7 @@ class TrackersFeature( singleEventProducer = { _, _, effect -> when (effect) { is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) - is Effect.OpenAppDetailsEffect -> SingleEvent.OpenAppDetailsEvent(effect.packageName) + is Effect.OpenAppDetailsEffect -> SingleEvent.OpenAppDetailsEvent(effect.appDesc) is Effect.TrackerToggleEffect -> { if (!effect.result) 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 441f39a..a259f0b 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 @@ -21,6 +21,8 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.core.content.ContextCompat +import androidx.fragment.app.add +import androidx.fragment.app.commit import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager @@ -36,6 +38,7 @@ import foundation.e.privacycentralapp.common.NavToolbarFragment import foundation.e.privacycentralapp.databinding.FragmentTrackersBinding import foundation.e.privacycentralapp.databinding.TrackersItemGraphBinding import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf +import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFragment import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect @@ -65,7 +68,11 @@ class TrackersFragment : displayToast(event.error) } is TrackersFeature.SingleEvent.OpenAppDetailsEvent -> { - displayToast(event.packageName) + requireActivity().supportFragmentManager.commit { + add<AppTrackersFragment>(R.id.container, args = AppTrackersFragment.buildArgs(event.appDesc.label.toString(), event.appDesc.packageName)) + setReorderingAllowed(true) + addToBackStack("apptrackers") + } } } } @@ -108,14 +115,6 @@ class TrackersFragment : ) } } - - // - // requireActivity().supportFragmentManager.commit { - // val bundle = bundleOf("TRACKER" to it.name) - // add<TrackerAppsFragment>(R.id.container, args = bundle) - // setReorderingAllowed(true) - // addToBackStack("trackers") - // } } override fun getTitle() = getString(R.string.trackers_title) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt new file mode 100644 index 0000000..a62ed16 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt @@ -0,0 +1,171 @@ +/* + * 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.apptrackers + +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.domain.usecases.TrackersStateUseCase +import foundation.e.privacymodules.permissions.data.ApplicationDescription +import foundation.e.privacymodules.trackers.Tracker +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +// Define a state machine for Tracker feature. +class AppTrackersFeature( + initialState: State, + coroutineScope: CoroutineScope, + reducer: Reducer<State, Effect>, + actor: Actor<State, Action, Effect>, + singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent> +) : BaseFeature<AppTrackersFeature.State, AppTrackersFeature.Action, AppTrackersFeature.Effect, AppTrackersFeature.SingleEvent>( + initialState, + actor, + reducer, + coroutineScope, + { message -> Log.d("TrackersFeature", message) }, + singleEventProducer +) { + data class State( + val appDesc: ApplicationDescription? = null, + val isBlockingActivated: Boolean = false, + val trackers: List<Tracker>? = null, + val whitelist: List<Int>? = null + ) { + fun getTrackersStatus(): List<Pair<Tracker, Boolean>>? { + if (trackers != null && whitelist != null) { + return trackers.map { it to (it.id !in whitelist) } + } else { + return null + } + } + } + + sealed class SingleEvent { + data class ErrorEvent(val error: String) : SingleEvent() + } + + sealed class Action { + data class InitAction(val packageName: String) : Action() + data class BlockAllToggleAction(val isBlocked: Boolean) : Action() + data class ToggleTrackerAction(val tracker: Tracker, val isBlocked: Boolean) : Action() + } + + sealed class Effect { + data class SetAppEffect(val appDesc: ApplicationDescription) : Effect() + data class AppTrackersBlockingActivatedEffect(val isBlockingActivated: Boolean) : Effect() + data class AvailableTrackersListEffect( + val isBlockingActivated: Boolean, + val trackers: List<Tracker>, + val whitelist: List<Int> + ) : Effect() + data class TrackersWhitelistUpdateEffect(val whitelist: List<Int>) : Effect() + + // object QuickPrivacyDisabledWarningEffect : Effect() + data class ErrorEffect(val message: String) : Effect() + } + + companion object { + fun create( + initialState: State = State(), + coroutineScope: CoroutineScope, + trackersStateUseCase: TrackersStateUseCase + ) = AppTrackersFeature( + initialState, coroutineScope, + reducer = { state, effect -> + when (effect) { + is Effect.SetAppEffect -> state.copy(appDesc = effect.appDesc) + is Effect.AvailableTrackersListEffect -> state.copy( + isBlockingActivated = effect.isBlockingActivated, + trackers = effect.trackers, + whitelist = effect.whitelist + ) + + is Effect.AppTrackersBlockingActivatedEffect -> + state.copy(isBlockingActivated = effect.isBlockingActivated) + + is Effect.TrackersWhitelistUpdateEffect -> + state.copy(whitelist = effect.whitelist) + is Effect.ErrorEffect -> state + } + }, + actor = { state, action -> + when (action) { + is Action.InitAction -> merge( + flow { + val appDesc = + trackersStateUseCase.getApplicationPermission(action.packageName) + emit(Effect.SetAppEffect(appDesc)) + + emit( + Effect.AvailableTrackersListEffect( + isBlockingActivated = !trackersStateUseCase.isWhitelisted( + appDesc.uid + ), + trackers = trackersStateUseCase.getTrackers(appDesc.uid), + whitelist = trackersStateUseCase.getTrackersWhitelistIds(appDesc.uid) + ) + ) + } + ) + is Action.BlockAllToggleAction -> + state.appDesc?.uid?.let { appUid -> + flow { + trackersStateUseCase.toggleAppWhitelist(appUid, !action.isBlocked) + + emit( + Effect.AppTrackersBlockingActivatedEffect( + !trackersStateUseCase.isWhitelisted( + appUid + ) + ) + ) + } + } ?: run { flowOf(Effect.ErrorEffect("No appDesc.")) } + is Action.ToggleTrackerAction -> { + state.appDesc?.uid?.let { appUid -> + flow { + trackersStateUseCase.blockTracker( + appUid, + action.tracker, + action.isBlocked + ) + emit( + Effect.TrackersWhitelistUpdateEffect( + trackersStateUseCase.getTrackersWhitelistIds(appUid) + ) + ) + } + } ?: run { flowOf(Effect.ErrorEffect("No appDesc.")) } + } + } + }, + singleEventProducer = { _, _, effect -> + when (effect) { + is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) + else -> null + } + } + ) + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt new file mode 100644 index 0000000..508aa5a --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt @@ -0,0 +1,126 @@ +/* + * 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.apptrackers + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import foundation.e.flowmvi.MVIView +import foundation.e.privacycentralapp.DependencyContainer +import foundation.e.privacycentralapp.PrivacyCentralApplication +import foundation.e.privacycentralapp.R +import foundation.e.privacycentralapp.common.NavToolbarFragment +import foundation.e.privacycentralapp.databinding.ApptrackersFragmentBinding +import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf +import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFeature.Action +import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFeature.SingleEvent +import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFeature.State +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect + +class AppTrackersFragment : + NavToolbarFragment(R.layout.apptrackers_fragment), + MVIView<State, Action> { + companion object { + private val PARAM_LABEL = "PARAM_LABEL" + private val PARAM_PACKAGE_NAME = "PARAM_PACKAGE_NAME" + fun buildArgs(label: String, packageName: String): Bundle = bundleOf( + PARAM_LABEL to label, + PARAM_PACKAGE_NAME to packageName + ) + } + + private val dependencyContainer: DependencyContainer by lazy { + (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer + } + + private val viewModel: AppTrackersViewModel by viewModels { + viewModelProviderFactoryOf { + dependencyContainer.appTrackersViewModelFactory.create() + } + } + + private lateinit var binding: ApptrackersFragmentBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launchWhenStarted { + viewModel.feature.takeView(this, this@AppTrackersFragment) + } + lifecycleScope.launchWhenStarted { + viewModel.feature.singleEvents.collect { event -> + when (event) { + is SingleEvent.ErrorEvent -> displayToast(event.error) + } + } + } + lifecycleScope.launchWhenStarted { + viewModel.submitAction( + Action.InitAction(requireArguments().getString(PARAM_PACKAGE_NAME)) + ) + } + } + + private fun displayToast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT) + .show() + } + + override fun getTitle(): String = requireArguments().getString(PARAM_LABEL) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = ApptrackersFragmentBinding.bind(view) + + // binding.blockAllToggle.setOnCheckedChangeListener { _, isChecked -> + // viewModel.submitAction(Action.BlockAllToggleAction(isChecked)) + // } + + binding.trackers.apply { + layoutManager = LinearLayoutManager(requireContext()) + setHasFixedSize(true) + adapter = ToggleTrackersAdapter(R.layout.apptrackers_item_tracker_toggle) { tracker, isBlocked -> + viewModel.submitAction( + Action.ToggleTrackerAction( + tracker, + isBlocked + ) + ) + } + } + } + + override fun render(state: AppTrackersFeature.State) { + // binding.blockAllToggle.isChecked = state.isBlockingActivated + + state.getTrackersStatus()?.let { + binding.trackers.isVisible = true + binding.trackers.post { + (binding.trackers.adapter as ToggleTrackersAdapter?)?.dataSet = it + } + binding.noTrackersYet.isVisible = false + } + } + + override fun actions(): Flow<Action> = viewModel.actions +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt new file mode 100644 index 0000000..8acbcac --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt @@ -0,0 +1,58 @@ +/* + * 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.apptrackers + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import foundation.e.privacycentralapp.common.Factory +import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class AppTrackersViewModel( + private val trackersStateUseCase: TrackersStateUseCase +) : ViewModel() { + + private val _actions = MutableSharedFlow<AppTrackersFeature.Action>() + val actions = _actions.asSharedFlow() + + val feature: AppTrackersFeature by lazy { + AppTrackersFeature.create( + coroutineScope = viewModelScope, + trackersStateUseCase = trackersStateUseCase + ) + } + + fun submitAction(action: AppTrackersFeature.Action) { + Log.d("TrackersViewModel", "submitting action") + viewModelScope.launch { + _actions.emit(action) + } + } +} + +class AppTrackersViewModelFactory( + private val trackersStateUseCase: TrackersStateUseCase +) : + Factory<AppTrackersViewModel> { + override fun create(): AppTrackersViewModel { + return AppTrackersViewModel(trackersStateUseCase) + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt new file mode 100644 index 0000000..f23ebf5 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt @@ -0,0 +1,68 @@ +/* + * 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.apptrackers + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.widget.SwitchCompat +import androidx.recyclerview.widget.RecyclerView +import foundation.e.privacycentralapp.R +import foundation.e.privacymodules.trackers.Tracker + +class ToggleTrackersAdapter( + private val itemsLayout: Int, + private val listener: (Tracker, Boolean) -> Unit +) : + RecyclerView.Adapter<ToggleTrackersAdapter.ViewHolder>() { + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val title: TextView = view.findViewById(R.id.title) + + val toggle: SwitchCompat = view.findViewById(R.id.toggle) + + fun bind(item: Pair<Tracker, Boolean>) { + title.text = item.first.label + toggle.isChecked = item.second + } + } + + var dataSet: List<Pair<Tracker, Boolean>> = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(itemsLayout, parent, false) + val holder = ViewHolder(view) + holder.toggle.setOnCheckedChangeListener { _, isChecked -> + listener(dataSet[holder.adapterPosition].first, isChecked) + } + return holder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val permission = dataSet[position] + holder.bind(permission) + } + + override fun getItemCount(): Int = dataSet.size +} |