From a484bf584f4163c8a0a1260e81d598fdec87ff3b Mon Sep 17 00:00:00 2001 From: jacquarg Date: Mon, 1 Nov 2021 21:24:09 +0100 Subject: Add trackers UI --- .../e/privacycentralapp/DependencyContainer.kt | 6 ++ .../e/privacycentralapp/common/AppsAdapter.kt | 67 ++++++++++++ .../privacycentralapp/common/ToggleAppsAdapter.kt | 2 - .../domain/usecases/TrackersStatisticsUseCase.kt | 16 +++ .../dummy/TrackTrackersPrivacyMock.kt | 12 +++ .../features/dashboard/DashboardFragment.kt | 16 --- .../features/trackers/TrackersFeature.kt | 77 +++++++++++++- .../features/trackers/TrackersFragment.kt | 118 +++++++++++++++++---- .../features/trackers/TrackersViewModel.kt | 28 ++++- 9 files changed, 302 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt (limited to 'app/src/main/java/foundation/e/privacycentralapp') diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt index ccb0a75..1ba235b 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt @@ -30,6 +30,7 @@ import foundation.e.privacycentralapp.features.dashboard.DashBoardViewModelFacto import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyViewModelFactory import foundation.e.privacycentralapp.features.location.FakeLocationViewModelFactory import foundation.e.privacycentralapp.features.location.LocationApiDelegate +import foundation.e.privacycentralapp.features.trackers.TrackersViewModelFactory import foundation.e.privacymodules.ipscrambler.IpScramblerModule import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule import foundation.e.privacymodules.location.FakeLocation @@ -83,6 +84,7 @@ class DependencyContainer constructor(val app: Application) { TrackersStatisticsUseCase(trackTrackersPrivacyModule) } + // ViewModelFactories val dashBoardViewModelFactory by lazy { DashBoardViewModelFactory(getQuickPrivacyStateUseCase, ipScramblingStateUseCase, trackersStatisticsUseCase) } @@ -96,4 +98,8 @@ class DependencyContainer constructor(val app: Application) { val internetPrivacyViewModelFactory by lazy { InternetPrivacyViewModelFactory(ipScramblerModule, getQuickPrivacyStateUseCase, ipScramblingStateUseCase, appListUseCase) } + + val trackersViewModelFactory by lazy { + TrackersViewModelFactory(getQuickPrivacyStateUseCase, trackersStatisticsUseCase, appListUseCase) + } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt new file mode 100644 index 0000000..d66ce76 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt @@ -0,0 +1,67 @@ +/* + * 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.common + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import foundation.e.privacycentralapp.R +import foundation.e.privacymodules.permissions.data.ApplicationDescription + +class AppsAdapter( + private val itemsLayout: Int, + private val listener: (String) -> Unit +) : + RecyclerView.Adapter() { + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val appName: TextView = view.findViewById(R.id.title) + + fun bind(item: ApplicationDescription) { + appName.text = item.label + + itemView.findViewById(R.id.icon).setImageDrawable(item.icon) + } + } + + var dataSet: List = 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.itemView.setOnClickListener { _ -> + listener(dataSet[holder.adapterPosition].packageName) + } + return holder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val app = dataSet[position] + holder.bind(app) + } + + override fun getItemCount(): Int = dataSet.size +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt index 71b5e97..1817a0d 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt @@ -22,7 +22,6 @@ import android.view.View import android.view.ViewGroup import android.widget.CheckBox import android.widget.ImageView -import android.widget.Switch import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import foundation.e.privacycentralapp.R @@ -60,7 +59,6 @@ class ToggleAppsAdapter( holder.togglePermission.setOnCheckedChangeListener { _, isChecked -> listener(dataSet[holder.adapterPosition].first.packageName, isChecked) } - view.findViewById(R.id.toggle) return holder } 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 93fbc08..33c3f64 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 @@ -26,4 +26,20 @@ class TrackersStatisticsUseCase( fun getPast24HoursTrackersCalls(): List { return trackTrackersPrivacyModule.getPast24HoursTrackersCalls() } + + fun getDayMonthYearStatistics(): Triple, List, List> { + return Triple( + trackTrackersPrivacyModule.getPast24HoursTrackersCalls(), + trackTrackersPrivacyModule.getPastMonthTrackersCalls(), + trackTrackersPrivacyModule.getPastYearTrackersCalls() + ) + } + + fun getDayMonthYearCounts(): Triple { + return Triple( + trackTrackersPrivacyModule.getPast24HoursTrackersCount(), + 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 76da6a2..55ca6ec 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackTrackersPrivacyMock.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/TrackTrackersPrivacyMock.kt @@ -30,6 +30,10 @@ class TrackTrackersPrivacyMock : ITrackTrackersPrivacyModule { ) } + override fun getPast24HoursTrackersCount(): Int { + return 30 + } + override fun getPastMonthTrackersCalls(): List { return listOf( 20000, 23000, 24130, 12500, 31000, 22000, @@ -40,6 +44,10 @@ class TrackTrackersPrivacyMock : ITrackTrackersPrivacyModule { ) } + override fun getPastMonthTrackersCount(): Int { + return 43 + } + override fun getPastYearTrackersCalls(): List { return listOf( 620000, 823000, 424130, 712500, 831000, 922000, @@ -47,6 +55,10 @@ class TrackTrackersPrivacyMock : ITrackTrackersPrivacyModule { ) } + override fun getPastYearTrackersCount(): Int { + return 46 + } + override fun getTrackersCount(): Int { return 72 } 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 abdf764..1b4ad39 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 @@ -18,13 +18,8 @@ package foundation.e.privacycentralapp.features.dashboard import android.content.Intent -import android.graphics.Color import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.style.ForegroundColorSpan import android.view.View -import android.widget.TextView import androidx.core.content.ContextCompat.getColor import androidx.fragment.app.activityViewModels import androidx.fragment.app.add @@ -143,17 +138,6 @@ class DashboardFragment : return getString(R.string.dashboard_title) } - private fun addClickToMore(textView: TextView) { - val clickToMore = SpannableString(getString(R.string.click_to_learn_more)) - clickToMore.setSpan( - ForegroundColorSpan(Color.parseColor("#007fff")), - 0, - clickToMore.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - textView.append(clickToMore) - } - override fun render(state: State) { binding.stateLabel.text = getString( 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 9400181..0394abb 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 @@ -22,11 +22,17 @@ 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.AppListUseCase +import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase +import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase import foundation.e.privacycentralapp.dummy.Tracker import foundation.e.privacycentralapp.dummy.TrackersDataSource +import foundation.e.privacymodules.permissions.data.ApplicationDescription 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 TrackersFeature( @@ -44,16 +50,28 @@ class TrackersFeature( singleEventProducer ) { data class State( + val dayStatistics: List? = null, + val dayTrackersCount: Int? = null, + val monthStatistics: List? = null, + val monthTrackersCount: Int? = null, + val yearStatistics: List? = null, + val yearTrackersCount: Int? = null, + val apps: List? = null, + val trackers: List = emptyList(), val currentSelectedTracker: Tracker? = null ) sealed class SingleEvent { data class ErrorEvent(val error: String) : SingleEvent() + data class OpenAppDetailsEvent(val packageName: String) : SingleEvent() object BlockerErrorEvent : SingleEvent() } sealed class Action { + object InitAction : Action() + data class ClickAppAction(val packageName: String) : Action() + object ObserveTrackers : Action() data class SetSelectedTracker(val tracker: Tracker) : Action() data class ToggleTrackerAction( @@ -64,6 +82,19 @@ class TrackersFeature( } sealed class Effect { + data class TrackersStatisticsLoadedEffect( + val dayStatistics: List? = null, + val dayTrackersCount: Int? = null, + val monthStatistics: List? = null, + val monthTrackersCount: Int? = null, + val yearStatistics: List? = null, + val yearTrackersCount: Int? = null + ) : Effect() + data class AvailableAppsListEffect( + val apps: List + ) : Effect() + data class OpenAppDetailsEffect(val packageName: String) : Effect() + object QuickPrivacyDisabledWarningEffect : Effect() data class TrackersLoadedEffect(val trackers: List) : Effect() data class TrackerSelectedEffect(val tracker: Tracker) : Effect() data class TrackerToggleEffect(val result: Boolean) : Effect() @@ -74,12 +105,25 @@ class TrackersFeature( companion object { fun create( initialState: State = State(), - coroutineScope: CoroutineScope + coroutineScope: CoroutineScope, + getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, + trackersStatisticsUseCase: TrackersStatisticsUseCase, + appListUseCase: AppListUseCase ) = TrackersFeature( initialState, coroutineScope, reducer = { state, effect -> when (effect) { - is Effect.TrackersLoadedEffect -> State(effect.trackers) + is Effect.TrackersStatisticsLoadedEffect -> state.copy( + dayStatistics = effect.dayStatistics, + dayTrackersCount = effect.dayTrackersCount, + monthStatistics = effect.monthStatistics, + monthTrackersCount = effect.monthTrackersCount, + yearStatistics = effect.yearStatistics, + yearTrackersCount = effect.yearTrackersCount + ) + is Effect.AvailableAppsListEffect -> state.copy(apps = effect.apps) + + is Effect.TrackersLoadedEffect -> State() is Effect.TrackerSelectedEffect -> state.copy(currentSelectedTracker = effect.tracker) is Effect.ErrorEffect -> state is Effect.TrackerToggleEffect -> { @@ -88,10 +132,37 @@ class TrackersFeature( is Effect.TrackerLoadedEffect -> { state.copy(currentSelectedTracker = effect.tracker) } + else -> state } }, actor = { state, action -> when (action) { + Action.InitAction -> merge( + flow { + val statistics = trackersStatisticsUseCase.getDayMonthYearStatistics() + val counts = trackersStatisticsUseCase.getDayMonthYearCounts() + emit( + Effect.TrackersStatisticsLoadedEffect( + dayStatistics = statistics.first, + dayTrackersCount = counts.first, + monthStatistics = statistics.second, + monthTrackersCount = counts.second, + yearStatistics = statistics.third, + yearTrackersCount = counts.third + ) + ) + }, + flow { + val apps = appListUseCase.getAppsUsingInternet() + emit(Effect.AvailableAppsListEffect(apps)) + } + ) + + is Action.ClickAppAction -> flowOf( + if (getPrivacyStateUseCase.isQuickPrivacyEnabled) + Effect.OpenAppDetailsEffect(action.packageName) + else Effect.QuickPrivacyDisabledWarningEffect + ) Action.ObserveTrackers -> TrackersDataSource.trackers.map { Effect.TrackersLoadedEffect( it @@ -131,9 +202,11 @@ class TrackersFeature( singleEventProducer = { _, _, effect -> when (effect) { is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) + is Effect.OpenAppDetailsEffect -> SingleEvent.OpenAppDetailsEvent(effect.packageName) is Effect.TrackerToggleEffect -> { if (!effect.result) SingleEvent.BlockerErrorEvent else null } + Effect.QuickPrivacyDisabledWarningEffect -> SingleEvent.ErrorEvent("Enabled Quick Privacy to use functionalities") 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 e3dc941..441f39a 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 @@ -19,24 +19,39 @@ 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 android.widget.Toast +import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import com.github.mikephil.charting.data.BarData +import com.github.mikephil.charting.data.BarDataSet +import com.github.mikephil.charting.data.BarEntry 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.AppsAdapter 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 kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers), MVIView { - private val viewModel: TrackersViewModel by viewModels() - private lateinit var trackersAdapter: TrackersAdapter + private val dependencyContainer: DependencyContainer by lazy { + (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer + } + + private val viewModel: TrackersViewModel by viewModels { + viewModelProviderFactoryOf { dependencyContainer.trackersViewModelFactory.create() } + } + + private lateinit var binding: FragmentTrackersBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,32 +59,99 @@ class TrackersFragment : viewModel.trackersFeature.takeView(this, this@TrackersFragment) } lifecycleScope.launchWhenStarted { - viewModel.submitAction(TrackersFeature.Action.ObserveTrackers) + viewModel.trackersFeature.singleEvents.collect { event -> + when (event) { + is TrackersFeature.SingleEvent.ErrorEvent -> { + displayToast(event.error) + } + is TrackersFeature.SingleEvent.OpenAppDetailsEvent -> { + displayToast(event.packageName) + } + } + } + } + + lifecycleScope.launchWhenStarted { + viewModel.submitAction(TrackersFeature.Action.InitAction) } } + 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 { - requireActivity().supportFragmentManager.commit { - val bundle = bundleOf("TRACKER" to it.name) - add(R.id.container, args = bundle) - setReorderingAllowed(true) - addToBackStack("trackers") + + binding = FragmentTrackersBinding.bind(view) + + listOf(binding.graphDay, binding.graphMonth, binding.graphYear).forEach { + it.graph.apply { + description = null + setTouchEnabled(false) + setDrawGridBackground(false) + setDrawBorders(false) + axisLeft.isEnabled = false + axisRight.isEnabled = false + xAxis.isEnabled = false + legend.isEnabled = false } - // viewModel.submitAction(TrackersFeature.Action.SetSelectedTracker(it)) } - view.findViewById(R.id.recylcer_view_trackers)?.apply { + + binding.apps.apply { layoutManager = LinearLayoutManager(requireContext()) setHasFixedSize(true) - adapter = trackersAdapter + adapter = AppsAdapter(R.layout.trackers_item_app) { packageName -> + viewModel.submitAction( + TrackersFeature.Action.ClickAppAction(packageName) + ) + } } + + // + // requireActivity().supportFragmentManager.commit { + // val bundle = bundleOf("TRACKER" to it.name) + // add(R.id.container, args = bundle) + // setReorderingAllowed(true) + // addToBackStack("trackers") + // } } - override fun getTitle() = getString(R.string.trackers) + override fun getTitle() = getString(R.string.trackers_title) override fun render(state: TrackersFeature.State) { - trackersAdapter.setData(state.trackers) + if (state.dayStatistics != null && state.dayTrackersCount != null) { + renderGraph(state.dayTrackersCount, state.dayStatistics, binding.graphDay) + } + + if (state.monthStatistics != null && state.monthTrackersCount != null) { + renderGraph(state.monthTrackersCount, state.monthStatistics, binding.graphMonth) + } + + if (state.yearStatistics != null && state.yearTrackersCount != null) { + renderGraph(state.yearTrackersCount, state.yearStatistics, binding.graphYear) + } + + state.apps?.let { + binding.apps.post { + (binding.apps.adapter as AppsAdapter?)?.dataSet = it + } + } + } + + private fun renderGraph(trackersCount: Int, data: List, graphBinding: TrackersItemGraphBinding) { + val trackersDataSet = BarDataSet( + data.mapIndexed { index, value -> BarEntry(index.toFloat(), value.toFloat()) }, + getString(R.string.trackers_count_label) + ).apply { + color = ContextCompat.getColor(requireContext(), R.color.purple_chart) + setDrawValues(false) + } + + graphBinding.graph.data = BarData(trackersDataSet) + graphBinding.graph.invalidate() + graphBinding.trackersCountLabel.text = getString(R.string.trackers_count_label, trackersCount) } override fun actions(): Flow = 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 index ee89887..12b66d4 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt @@ -20,17 +20,30 @@ package foundation.e.privacycentralapp.features.trackers import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import foundation.e.privacycentralapp.common.Factory +import foundation.e.privacycentralapp.domain.usecases.AppListUseCase +import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase +import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch -class TrackersViewModel : ViewModel() { +class TrackersViewModel( + private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, + private val trackersStatisticsUseCase: TrackersStatisticsUseCase, + private val appListUseCase: AppListUseCase +) : ViewModel() { private val _actions = MutableSharedFlow() val actions = _actions.asSharedFlow() val trackersFeature: TrackersFeature by lazy { - TrackersFeature.create(coroutineScope = viewModelScope) + TrackersFeature.create( + coroutineScope = viewModelScope, + getPrivacyStateUseCase = getQuickPrivacyStateUseCase, + trackersStatisticsUseCase = trackersStatisticsUseCase, + appListUseCase = appListUseCase + ) } fun submitAction(action: TrackersFeature.Action) { @@ -40,3 +53,14 @@ class TrackersViewModel : ViewModel() { } } } + +class TrackersViewModelFactory( + private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, + private val trackersStatisticsUseCase: TrackersStatisticsUseCase, + private val appListUseCase: AppListUseCase +) : + Factory { + override fun create(): TrackersViewModel { + return TrackersViewModel(getQuickPrivacyStateUseCase, trackersStatisticsUseCase, appListUseCase) + } +} -- cgit v1.2.1