aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/foundation/e/advancedprivacy/features
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/foundation/e/advancedprivacy/features')
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt62
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt123
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt81
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt64
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt172
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt17
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt67
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt152
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt12
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt102
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt72
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt67
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt149
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt31
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt128
15 files changed, 1018 insertions, 281 deletions
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt
new file mode 100644
index 0000000..f00dff8
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION, 2022 - 2023 MURENA SAS
+ *
+ * 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.advancedprivacy.features.trackers
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding
+
+class AppsAdapter(
+ private val viewModel: TrackersViewModel
+) :
+ RecyclerView.Adapter<AppsAdapter.ViewHolder>() {
+
+ class ViewHolder(view: View, private val parentViewModel: TrackersViewModel) : RecyclerView.ViewHolder(view) {
+ val binding = TrackersItemAppBinding.bind(view)
+ fun bind(item: AppWithTrackersCount) {
+ binding.icon.setImageDrawable(item.app.icon)
+ binding.title.text = item.app.label
+ binding.counts.text = itemView.context.getString(R.string.trackers_list_app_trackers_counts, item.trackersCount.toString())
+ itemView.setOnClickListener {
+ parentViewModel.onClickApp(item.app)
+ }
+ }
+ }
+
+ var dataSet: List<AppWithTrackersCount> = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.trackers_item_app, parent, false)
+ return ViewHolder(view, viewModel)
+ }
+
+ 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/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt
new file mode 100644
index 0000000..2420410
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * 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.advancedprivacy.features.trackers
+
+import android.content.Context
+import android.content.res.Resources
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.divider.MaterialDividerItemDecoration
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.databinding.TrackersListBinding
+
+const val TAB_APPS = 0
+private const val TAB_TRACKERS = 1
+
+class ListsTabPagerAdapter(
+ private val context: Context,
+ private val viewModel: TrackersViewModel,
+) : RecyclerView.Adapter<ListsTabPagerAdapter.ListsTabViewHolder>() {
+ private var apps: List<AppWithTrackersCount> = emptyList()
+ private var trackers: List<TrackerWithAppsCount> = emptyList()
+
+ fun updateDataSet(apps: List<AppWithTrackersCount>?, trackers: List<TrackerWithAppsCount>?) {
+ this.apps = apps ?: emptyList()
+ this.trackers = trackers ?: emptyList()
+ notifyDataSetChanged()
+ }
+
+ override fun getItemViewType(position: Int): Int = position
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListsTabViewHolder {
+ val binding = TrackersListBinding.inflate(LayoutInflater.from(context), parent, false)
+ return when (viewType) {
+ TAB_APPS -> {
+ ListsTabViewHolder.AppsListViewHolder(binding, viewModel)
+ }
+ else -> {
+ ListsTabViewHolder.TrackersListViewHolder(binding, viewModel)
+ }
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return 2
+ }
+
+ override fun onBindViewHolder(holder: ListsTabViewHolder, position: Int) {
+ when (position) {
+ TAB_APPS -> {
+ (holder as ListsTabViewHolder.AppsListViewHolder).onBind(apps)
+ }
+ TAB_TRACKERS -> {
+ (holder as ListsTabViewHolder.TrackersListViewHolder).onBind(trackers)
+ }
+ }
+ }
+
+ sealed class ListsTabViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ protected fun setupRecyclerView(recyclerView: RecyclerView) {
+ recyclerView.apply {
+ layoutManager = LinearLayoutManager(context)
+ setHasFixedSize(true)
+ addItemDecoration(
+ MaterialDividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply {
+ dividerColor = ContextCompat.getColor(context, R.color.divider)
+ dividerInsetStart = 16.dpToPx()
+ dividerInsetEnd = 16.dpToPx()
+ }
+ )
+ }
+ }
+
+ private fun Int.dpToPx(): Int {
+ return (this * Resources.getSystem().displayMetrics.density).toInt()
+ }
+
+ class AppsListViewHolder(
+ private val binding: TrackersListBinding,
+ private val viewModel: TrackersViewModel
+ ) : ListsTabViewHolder(binding.root) {
+ init {
+ setupRecyclerView(binding.list)
+ binding.list.adapter = AppsAdapter(viewModel)
+ }
+
+ fun onBind(apps: List<AppWithTrackersCount>) {
+ (binding.list.adapter as AppsAdapter).dataSet = apps
+ }
+ }
+
+ class TrackersListViewHolder(
+ private val binding: TrackersListBinding,
+ private val viewModel: TrackersViewModel
+ ) : ListsTabViewHolder(binding.root) {
+ init {
+ setupRecyclerView(binding.list)
+ binding.list.adapter = TrackersAdapter(viewModel)
+ }
+
+ fun onBind(trackers: List<TrackerWithAppsCount>) {
+ (binding.list.adapter as TrackersAdapter).dataSet = trackers
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt
new file mode 100644
index 0000000..183a5ca
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * 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.advancedprivacy.features.trackers
+
+import android.content.Context
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.ClickableSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.UnderlineSpan
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import foundation.e.advancedprivacy.R
+
+const val URL_LEARN_MORE_ABOUT_TRACKERS = "https://doc.e.foundation/support-topics/advanced_privacy#trackers-blocker"
+
+fun setupDisclaimerBlock(view: TextView, onClickLearnMore: () -> Unit) {
+ with(view) {
+ linksClickable = true
+ isClickable = true
+ movementMethod = android.text.method.LinkMovementMethod.getInstance()
+ text = buildSpan(view.context, onClickLearnMore)
+ }
+}
+
+private fun buildSpan(context: Context, onClickLearnMore: () -> Unit): SpannableString {
+ val start = context.getString(R.string.trackercontroldisclaimer_start)
+ val body = context.getString(R.string.trackercontroldisclaimer_body)
+ val link = context.getString(R.string.trackercontroldisclaimer_link)
+
+ val spannable = SpannableString("$start $body $link")
+
+ val startEndIndex = start.length + 1
+ val linkStartIndex = startEndIndex + body.length + 1
+ val linkEndIndex = spannable.length
+ spannable.setSpan(
+ ForegroundColorSpan(ContextCompat.getColor(context, R.color.primary_text)),
+ 0,
+ startEndIndex,
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+ )
+
+ spannable.setSpan(
+ ForegroundColorSpan(ContextCompat.getColor(context, R.color.disabled)),
+ startEndIndex,
+ linkStartIndex,
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+ )
+
+ spannable.setSpan(
+ ForegroundColorSpan(ContextCompat.getColor(context, R.color.accent)),
+ linkStartIndex,
+ linkEndIndex,
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+ )
+ spannable.setSpan(UnderlineSpan(), linkStartIndex, linkEndIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
+ spannable.setSpan(
+ object : ClickableSpan() {
+ override fun onClick(p0: View) {
+ onClickLearnMore.invoke()
+ }
+ },
+ linkStartIndex, linkEndIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+ )
+ return spannable
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt
new file mode 100644
index 0000000..3270bf3
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * 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.advancedprivacy.features.trackers
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding
+
+class TrackersAdapter(
+ val viewModel: TrackersViewModel
+) :
+ RecyclerView.Adapter<TrackersAdapter.ViewHolder>() {
+
+ class ViewHolder(view: View, private val parentViewModel: TrackersViewModel) : RecyclerView.ViewHolder(view) {
+ val binding = TrackersItemAppBinding.bind(view)
+ init {
+ binding.icon.isVisible = false
+ }
+ fun bind(item: TrackerWithAppsCount) {
+ binding.title.text = item.tracker.label
+ binding.counts.text = itemView.context.getString(R.string.trackers_list_tracker_apps_counts, item.appsCount.toString())
+ itemView.setOnClickListener {
+ parentViewModel.onClickTracker(item.tracker)
+ }
+ }
+ }
+
+ var dataSet: List<TrackerWithAppsCount> = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.trackers_item_app, parent, false)
+ return ViewHolder(view, viewModel)
+ }
+
+ 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/advancedprivacy/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt
index 132fa3b..b016c5e 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt
@@ -28,6 +28,7 @@ import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.UnderlineSpan
import android.view.View
+import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@@ -35,12 +36,13 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
-import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.tabs.TabLayoutMediator
import foundation.e.advancedprivacy.R
-import foundation.e.advancedprivacy.common.AppsAdapter
import foundation.e.advancedprivacy.common.GraphHolder
import foundation.e.advancedprivacy.common.NavToolbarFragment
-import foundation.e.advancedprivacy.common.setToolTipForAsterisk
+import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPosition
+import foundation.e.advancedprivacy.common.extensions.updatePagerHeightForChild
import foundation.e.advancedprivacy.databinding.FragmentTrackersBinding
import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding
import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics
@@ -50,32 +52,98 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) {
private val viewModel: TrackersViewModel by viewModel()
- private var _binding: FragmentTrackersBinding? = null
- private val binding get() = _binding!!
+ private lateinit var binding: FragmentTrackersBinding
private var dayGraphHolder: GraphHolder? = null
private var monthGraphHolder: GraphHolder? = null
private var yearGraphHolder: GraphHolder? = null
+ private lateinit var tabAdapter: ListsTabPagerAdapter
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- _binding = FragmentTrackersBinding.bind(view)
+ binding = FragmentTrackersBinding.bind(view)
dayGraphHolder = GraphHolder(binding.graphDay.graph, requireContext(), false)
monthGraphHolder = GraphHolder(binding.graphMonth.graph, requireContext(), false)
yearGraphHolder = GraphHolder(binding.graphYear.graph, requireContext(), false)
- binding.apps.apply {
- layoutManager = LinearLayoutManager(requireContext())
- setHasFixedSize(true)
- adapter = AppsAdapter(R.layout.trackers_item_app) { appUid ->
- viewModel.submitAction(
- TrackersViewModel.Action.ClickAppAction(appUid)
- )
+ tabAdapter = ListsTabPagerAdapter(requireContext(), viewModel)
+ binding.listsPager.adapter = tabAdapter
+
+ TabLayoutMediator(binding.listsTabs, binding.listsPager) { tab, position ->
+ tab.text = getString(
+ when (position) {
+ TAB_APPS -> R.string.trackers_toggle_list_apps
+ else -> R.string.trackers_toggle_list_trackers
+ }
+ )
+ }.attach()
+
+ binding.listsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageScrollStateChanged(state: Int) {
+ super.onPageScrollStateChanged(state)
+ if (state == ViewPager2.SCROLL_STATE_IDLE) {
+ updatePagerHeight()
+ }
+ }
+ })
+
+ setupTrackersInfos()
+
+ listenViewModel()
+ }
+
+ private fun listenViewModel() {
+ with(viewLifecycleOwner) {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
+ }
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect(::handleEvents)
+ }
+ }
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.navigate.collect(findNavController()::navigate)
+ }
+ }
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
+ }
+ }
+
+ private fun handleEvents(event: TrackersViewModel.SingleEvent) {
+ when (event) {
+ is TrackersViewModel.SingleEvent.ErrorEvent -> {
+ displayToast(event.error)
+ }
+ is TrackersViewModel.SingleEvent.OpenUrl -> {
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, event.url))
+ } catch (e: ActivityNotFoundException) {
+ Toast.makeText(
+ requireContext(),
+ R.string.error_no_activity_view_url,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
}
}
+ }
+ private fun setupTrackersInfos() {
val infoText = getString(R.string.trackers_info)
val moreText = getString(R.string.trackers_info_more)
@@ -92,7 +160,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) {
spannable.setSpan(
object : ClickableSpan() {
override fun onClick(p0: View) {
- viewModel.submitAction(TrackersViewModel.Action.ClickLearnMore)
+ viewModel.onClickLearnMore()
}
},
startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
@@ -104,71 +172,44 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) {
movementMethod = LinkMovementMethod.getInstance()
text = spannable
}
+ }
- setToolTipForAsterisk(
- textView = binding.trackersAppsListTitle,
- textId = R.string.trackers_applist_title,
- tooltipTextId = R.string.trackers_applist_infos
- )
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- render(viewModel.state.value)
- viewModel.state.collect(::render)
+ private var oldPosition = -1
+ private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
+ binding.listsPager.findViewHolderForAdapterPosition(binding.listsPager.currentItem)
+ .let { currentViewHolder ->
+ currentViewHolder?.itemView?.let { binding.listsPager.updatePagerHeightForChild(it) }
}
- }
+ }
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.singleEvents.collect { event ->
- when (event) {
- is TrackersViewModel.SingleEvent.ErrorEvent -> {
- displayToast(event.error)
- }
- is TrackersViewModel.SingleEvent.OpenUrl -> {
- try {
- startActivity(Intent(Intent.ACTION_VIEW, event.url))
- } catch (e: ActivityNotFoundException) {
- Toast.makeText(
- requireContext(),
- R.string.error_no_activity_view_url,
- Toast.LENGTH_SHORT
- ).show()
- }
- }
- }
- }
+ private fun updatePagerHeight() {
+ with(binding.listsPager) {
+ val position = currentItem
+ if (position == oldPosition) return
+ if (oldPosition > 0) {
+ val oldItem = findViewHolderForAdapterPosition(oldPosition)?.itemView
+ oldItem?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
}
- }
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.navigate.collect(findNavController()::navigate)
- }
- }
+ val newItem = findViewHolderForAdapterPosition(position)?.itemView
+ newItem?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.doOnStartedState()
- }
+ oldPosition = position
+ adapter?.notifyItemChanged(position)
}
}
private fun displayToast(message: String) {
- Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
- .show()
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
private fun render(state: TrackersState) {
state.dayStatistics?.let { renderGraph(it, dayGraphHolder!!, binding.graphDay) }
state.monthStatistics?.let { renderGraph(it, monthGraphHolder!!, binding.graphMonth) }
state.yearStatistics?.let { renderGraph(it, yearGraphHolder!!, binding.graphYear) }
+ updatePagerHeight()
- state.apps?.let {
- binding.apps.post {
- (binding.apps.adapter as AppsAdapter?)?.dataSet = it
- }
- }
+ tabAdapter.updateDataSet(state.apps, state.trackers)
}
private fun renderGraph(
@@ -191,9 +232,14 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) {
override fun onDestroyView() {
super.onDestroyView()
+ kotlin.runCatching {
+ if (oldPosition >= 0) {
+ val oldItem = binding.listsPager.findViewHolderForAdapterPosition(oldPosition)
+ oldItem?.itemView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
+ }
+ }
dayGraphHolder = null
monthGraphHolder = null
yearGraphHolder = null
- _binding = null
}
}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt
index 13719e4..7f5fdfe 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt
@@ -1,4 +1,5 @@
/*
+ * Copyright (C) 2023 MURENA SAS
* Copyright (C) 2022 E FOUNDATION
*
* This program is free software: you can redistribute it and/or modify
@@ -17,12 +18,24 @@
package foundation.e.advancedprivacy.features.trackers
-import foundation.e.advancedprivacy.domain.entities.AppWithCounts
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
data class TrackersState(
val dayStatistics: TrackersPeriodicStatistics? = null,
val monthStatistics: TrackersPeriodicStatistics? = null,
val yearStatistics: TrackersPeriodicStatistics? = null,
- val apps: List<AppWithCounts>? = null,
+ val apps: List<AppWithTrackersCount>? = null,
+ val trackers: List<TrackerWithAppsCount>? = null
+)
+
+data class AppWithTrackersCount(
+ val app: ApplicationDescription,
+ val trackersCount: Int = 0
+)
+
+data class TrackerWithAppsCount(
+ val tracker: Tracker,
+ val appsCount: Int = 0
)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt
index 8a5d0f0..31da8ca 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt
@@ -22,27 +22,24 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavDirections
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase
import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TrackersViewModel(
- private val trackersStatisticsUseCase: TrackersStatisticsUseCase
+ private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
+ private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase
) : ViewModel() {
- companion object {
- private const val URL_LEARN_MORE_ABOUT_TRACKERS =
- "https://doc.e.foundation/support-topics/advanced_privacy#trackers-blocker"
- }
-
private val _state = MutableStateFlow(TrackersState())
val state = _state.asStateFlow()
@@ -53,46 +50,40 @@ class TrackersViewModel(
val navigate = _navigate.asSharedFlow()
suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
- merge(
- trackersStatisticsUseCase.listenUpdates().map {
- trackersStatisticsUseCase.getDayMonthYearStatistics()
- .let { (day, month, year) ->
- _state.update { s ->
- s.copy(
- dayStatistics = day,
- monthStatistics = month,
- yearStatistics = year
- )
- }
+ trackersStatisticsUseCase.listenUpdates().collect {
+ trackersStatisticsUseCase.getDayMonthYearStatistics()
+ .let { (day, month, year) ->
+ _state.update { s ->
+ s.copy(
+ dayStatistics = day,
+ monthStatistics = month,
+ yearStatistics = year
+ )
}
- },
- trackersStatisticsUseCase.getAppsWithCounts().map {
- _state.update { s -> s.copy(apps = it) }
+ }
+
+ trackersAndAppsListsUseCase.getAppsAndTrackersCounts().let { (appList, trackerList) ->
+ _state.update {
+ it.copy(apps = appList, trackers = trackerList)
+ }
}
- ).collect {}
+ }
}
- fun submitAction(action: Action) = viewModelScope.launch {
- when (action) {
- is Action.ClickAppAction -> actionClickApp(action)
- is Action.ClickLearnMore ->
- _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
- }
+ fun onClickTracker(tracker: Tracker) = viewModelScope.launch {
+ _navigate.emit(TrackersFragmentDirections.gotoTrackerDetailsFragment(trackerId = tracker.id))
}
- private suspend fun actionClickApp(action: Action.ClickAppAction) {
- state.value.apps?.find { it.uid == action.appUid }?.let {
- _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = it.uid))
- }
+ fun onClickApp(app: ApplicationDescription) = viewModelScope.launch {
+ _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = app.uid))
+ }
+
+ fun onClickLearnMore() = viewModelScope.launch {
+ _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
}
sealed class SingleEvent {
data class ErrorEvent(val error: String) : SingleEvent()
data class OpenUrl(val url: Uri) : SingleEvent()
}
-
- sealed class Action {
- data class ClickAppAction(val appUid: Int) : Action()
- object ClickLearnMore : Action()
- }
}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt
index 7fb9ca6..85c5350 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt
@@ -23,16 +23,19 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
+import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.snackbar.Snackbar
import foundation.e.advancedprivacy.R
import foundation.e.advancedprivacy.common.NavToolbarFragment
import foundation.e.advancedprivacy.databinding.ApptrackersFragmentBinding
+import foundation.e.advancedprivacy.features.trackers.setupDisclaimerBlock
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
@@ -42,8 +45,7 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) {
private val args: AppTrackersFragmentArgs by navArgs()
private val viewModel: AppTrackersViewModel by viewModel { parametersOf(args.appUid) }
- private var _binding: ApptrackersFragmentBinding? = null
- private val binding get() = _binding!!
+ private lateinit var binding: ApptrackersFragmentBinding
override fun getTitle(): CharSequence {
return ""
@@ -56,96 +58,111 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- _binding = ApptrackersFragmentBinding.bind(view)
+ binding = ApptrackersFragmentBinding.bind(view)
binding.blockAllToggle.setOnClickListener {
- viewModel.submitAction(AppTrackersViewModel.Action.BlockAllToggleAction(binding.blockAllToggle.isChecked))
- }
- binding.btnReset.setOnClickListener {
- viewModel.submitAction(AppTrackersViewModel.Action.ResetAllTrackers)
+ viewModel.onToggleBlockAll(binding.blockAllToggle.isChecked)
}
+ binding.btnReset.setOnClickListener { viewModel.onClickResetAllTrackers() }
- binding.trackers.apply {
+ binding.list.apply {
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
- adapter = ToggleTrackersAdapter(
- R.layout.apptrackers_item_tracker_toggle,
- onToggleSwitch = { tracker, isBlocked ->
- viewModel.submitAction(AppTrackersViewModel.Action.ToggleTrackerAction(tracker, isBlocked))
- },
- onClickTitle = { viewModel.submitAction(AppTrackersViewModel.Action.ClickTracker(it)) },
+ addItemDecoration(
+ MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL).apply {
+ dividerColor = ContextCompat.getColor(requireContext(), R.color.divider)
+ }
)
+ adapter = ToggleTrackersAdapter(viewModel)
}
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.singleEvents.collect { event ->
- when (event) {
- is AppTrackersViewModel.SingleEvent.ErrorEvent ->
- displayToast(getString(event.errorResId))
- is AppTrackersViewModel.SingleEvent.OpenUrl ->
- try {
- startActivity(Intent(Intent.ACTION_VIEW, event.url))
- } catch (e: ActivityNotFoundException) {
- Toast.makeText(
- requireContext(),
- R.string.error_no_activity_view_url,
- Toast.LENGTH_SHORT
- ).show()
- }
- is AppTrackersViewModel.SingleEvent.ToastTrackersControlDisabled ->
- Snackbar.make(
- binding.root,
- R.string.apptrackers_tracker_control_disabled_message,
- Snackbar.LENGTH_LONG
- ).show()
- }
+ listenViewModel()
+
+ setupDisclaimerBlock(binding.disclaimerBlockTrackers.root, viewModel::onClickLearnMore)
+ }
+
+ private fun listenViewModel() {
+ with(viewLifecycleOwner) {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect(::handleEvents)
}
}
- }
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.doOnStartedState()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
}
- }
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- render(viewModel.state.value)
- viewModel.state.collect(::render)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
}
}
}
+ private fun handleEvents(event: AppTrackersViewModel.SingleEvent) {
+ when (event) {
+ is AppTrackersViewModel.SingleEvent.ErrorEvent ->
+ displayToast(getString(event.errorResId))
+
+ is AppTrackersViewModel.SingleEvent.OpenUrl ->
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, event.url))
+ } catch (e: ActivityNotFoundException) {
+ Toast.makeText(
+ requireContext(),
+ R.string.error_no_activity_view_url,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ is AppTrackersViewModel.SingleEvent.ToastTrackersControlDisabled ->
+ Snackbar.make(
+ binding.root,
+ R.string.apptrackers_tracker_control_disabled_message,
+ Snackbar.LENGTH_LONG
+ ).show()
+ }
+ }
private fun render(state: AppTrackersState) {
setTitle(state.appDesc?.label)
- binding.trackersCountSummary.text = if (state.getTrackersCount() == 0) ""
- else getString(
- R.string.apptrackers_trackers_count_summary,
- state.getBlockedTrackersCount(),
- state.getTrackersCount(),
- state.blocked,
- state.leaked
- )
+ binding.subtitle.text = getString(R.string.apptrackers_subtitle, state.appDesc?.label)
+ binding.dataDetectedTrackers.apply {
+ primaryMessage.setText(R.string.apptrackers_detected_tracker_primary)
+ number.text = state.getTrackersCount().toString()
+ secondaryMessage.setText(R.string.apptrackers_detected_tracker_secondary)
+ }
+
+ binding.dataBlockedTrackers.apply {
+ primaryMessage.setText(R.string.apptrackers_blocked_tracker_primary)
+ number.text = state.getBlockedTrackersCount().toString()
+ secondaryMessage.setText(R.string.apptrackers_blocked_tracker_secondary)
+ }
+
+ binding.dataBlockedLeaks.apply {
+ primaryMessage.setText(R.string.apptrackers_blocked_leaks_primary)
+ number.text = state.blocked.toString()
+ secondaryMessage.text = getString(R.string.apptrackers_blocked_leaks_secondary, state.leaked.toString())
+ }
binding.blockAllToggle.isChecked = state.isBlockingActivated
- val trackersStatus = state.getTrackersStatus()
- if (!trackersStatus.isNullOrEmpty()) {
- binding.trackersListTitle.isVisible = state.isBlockingActivated
- binding.trackers.isVisible = true
- binding.trackers.post {
- (binding.trackers.adapter as ToggleTrackersAdapter?)?.updateDataSet(
- trackersStatus,
- state.isBlockingActivated
- )
+ val trackersStatus = state.trackersWithBlockedList
+ if (!trackersStatus.isEmpty()) {
+ binding.listTitle.isVisible = true
+ binding.list.isVisible = true
+ binding.list.post {
+ (binding.list.adapter as ToggleTrackersAdapter?)?.updateDataSet(trackersStatus)
}
binding.noTrackersYet.isVisible = false
binding.btnReset.isVisible = true
} else {
- binding.trackersListTitle.isVisible = false
- binding.trackers.isVisible = false
+ binding.listTitle.isVisible = false
+ binding.list.isVisible = false
binding.noTrackersYet.isVisible = true
binding.noTrackersYet.text = getString(
when {
@@ -157,9 +174,4 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) {
binding.btnReset.isVisible = state.isBlockingActivated && !state.isWhitelistEmpty
}
}
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt
index a597da6..cea99a6 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt
@@ -24,19 +24,13 @@ import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
data class AppTrackersState(
val appDesc: ApplicationDescription? = null,
val isBlockingActivated: Boolean = false,
- val trackersWithWhiteList: List<Pair<Tracker, Boolean>>? = null,
+ val trackersWithBlockedList: List<Pair<Tracker, Boolean>> = emptyList(),
val leaked: Int = 0,
val blocked: Int = 0,
val isTrackersBlockingEnabled: Boolean = false,
val isWhitelistEmpty: Boolean = true,
- val showQuickPrivacyDisabledMessage: Boolean = false,
) {
- fun getTrackersStatus(): List<Pair<Tracker, Boolean>>? {
- return trackersWithWhiteList?.map { it.first to !it.second }
- }
+ fun getTrackersCount() = trackersWithBlockedList.size
- fun getTrackersCount() = trackersWithWhiteList?.size ?: 0
- fun getBlockedTrackersCount(): Int = if (isTrackersBlockingEnabled && isBlockingActivated)
- trackersWithWhiteList?.count { !it.second } ?: 0
- else 0
+ fun getBlockedTrackersCount(): Int = trackersWithBlockedList.count { it.second }
}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt
index 8740779..00ad365 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt
@@ -24,9 +24,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
import foundation.e.advancedprivacy.domain.entities.TrackerMode
+import foundation.e.advancedprivacy.domain.usecases.AppTrackersUseCase
import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase
import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.advancedprivacy.features.trackers.URL_LEARN_MORE_ABOUT_TRACKERS
import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -41,6 +43,7 @@ import kotlinx.coroutines.withContext
class AppTrackersViewModel(
private val app: ApplicationDescription,
+ private val appTrackersUseCase: AppTrackersUseCase,
private val trackersStateUseCase: TrackersStateUseCase,
private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase
@@ -56,17 +59,10 @@ class AppTrackersViewModel(
val singleEvents = _singleEvents.asSharedFlow()
init {
- viewModelScope.launch(Dispatchers.IO) {
- _state.update {
- it.copy(
- appDesc = app,
- isBlockingActivated = !trackersStateUseCase.isWhitelisted(app),
- trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(
- app
- ),
- isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
- )
- }
+ _state.update {
+ it.copy(
+ appDesc = app,
+ )
}
}
@@ -79,80 +75,71 @@ class AppTrackersViewModel(
).collect { }
}
- fun submitAction(action: Action) = viewModelScope.launch {
- when (action) {
- is Action.BlockAllToggleAction -> blockAllToggleAction(action)
- is Action.ToggleTrackerAction -> toggleTrackerAction(action)
- is Action.ClickTracker -> actionClickTracker(action)
- is Action.ResetAllTrackers -> resetAllTrackers()
+ fun onClickLearnMore() {
+ viewModelScope.launch {
+ _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
}
}
- private suspend fun blockAllToggleAction(action: Action.BlockAllToggleAction) {
- withContext(Dispatchers.IO) {
+ fun onToggleBlockAll(isBlocked: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
if (!state.value.isTrackersBlockingEnabled) {
_singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
}
- trackersStateUseCase.toggleAppWhitelist(app, !action.isBlocked)
- _state.update {
- it.copy(
- isBlockingActivated = !trackersStateUseCase.isWhitelisted(app)
- )
- }
+ appTrackersUseCase.toggleAppWhitelist(app, isBlocked)
+ updateWhitelist()
}
}
- private suspend fun toggleTrackerAction(action: Action.ToggleTrackerAction) {
- withContext(Dispatchers.IO) {
+ fun onToggleTracker(tracker: Tracker, isBlocked: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
if (!state.value.isTrackersBlockingEnabled) {
_singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
}
- if (state.value.isBlockingActivated) {
- trackersStateUseCase.blockTracker(app, action.tracker, action.isBlocked)
- updateWhitelist()
- }
+ trackersStateUseCase.blockTracker(app, tracker, isBlocked)
+ updateWhitelist()
}
}
- private suspend fun actionClickTracker(action: Action.ClickTracker) {
- withContext(Dispatchers.IO) {
- action.tracker.exodusId?.let {
- try {
- _singleEvents.emit(
- SingleEvent.OpenUrl(
- Uri.parse(exodusBaseUrl + it)
- )
- )
- } catch (e: Exception) {
- }
- }
+ fun onClickTracker(tracker: Tracker) {
+ viewModelScope.launch(Dispatchers.IO) {
+ tracker.exodusId?.let {
+ runCatching { Uri.parse(exodusBaseUrl + it) }.getOrNull()
+ }?.let { _singleEvents.emit(SingleEvent.OpenUrl(it)) }
}
}
- private suspend fun resetAllTrackers() {
- withContext(Dispatchers.IO) {
- trackersStateUseCase.clearWhitelist(app)
+ fun onClickResetAllTrackers() {
+ viewModelScope.launch(Dispatchers.IO) {
+ appTrackersUseCase.clearWhitelist(app)
updateWhitelist()
}
}
- private fun fetchStatistics() {
- val (blocked, leaked) = trackersStatisticsUseCase.getCalls(app)
- return _state.update { s ->
+
+ private suspend fun fetchStatistics() = withContext(Dispatchers.IO) {
+ val (blocked, leaked) = appTrackersUseCase.getCalls(app)
+ val trackersWithBlockedList = appTrackersUseCase.getTrackersWithBlockedList(app)
+
+ _state.update { s ->
s.copy(
- trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app),
leaked = leaked,
blocked = blocked,
- isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
+ isBlockingActivated = !trackersStateUseCase.isWhitelisted(app),
+ isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app),
+ trackersWithBlockedList = trackersWithBlockedList
)
}
}
- private fun updateWhitelist() {
+ private suspend fun updateWhitelist() = withContext(Dispatchers.IO) {
_state.update { s ->
s.copy(
- trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app),
- isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
+ isBlockingActivated = !trackersStateUseCase.isWhitelisted(app),
+ trackersWithBlockedList = appTrackersUseCase.enrichWithBlockedState(
+ app, s.trackersWithBlockedList.map { it.first }
+ ),
+ isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app),
)
}
}
@@ -162,11 +149,4 @@ class AppTrackersViewModel(
data class OpenUrl(val url: Uri) : SingleEvent()
object ToastTrackersControlDisabled : SingleEvent()
}
-
- sealed class Action {
- data class BlockAllToggleAction(val isBlocked: Boolean) : Action()
- data class ToggleTrackerAction(val tracker: Tracker, val isBlocked: Boolean) : Action()
- data class ClickTracker(val tracker: Tracker) : Action()
- object ResetAllTrackers : Action()
- }
}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt
index ef845b6..1d49905 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt
@@ -1,4 +1,5 @@
/*
+ * Copyright (C) 2023 MURENA SAS
* Copyright (C) 2021 E FOUNDATION
*
* This program is free software: you can redistribute it and/or modify
@@ -20,72 +21,67 @@ package foundation.e.advancedprivacy.features.trackers.apptrackers
import android.text.SpannableString
import android.text.style.UnderlineSpan
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
-import android.widget.Switch
-import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.databinding.ApptrackersItemTrackerToggleBinding
import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
class ToggleTrackersAdapter(
- private val itemsLayout: Int,
- private val onToggleSwitch: (Tracker, Boolean) -> Unit,
- private val onClickTitle: (Tracker) -> Unit
+ private val viewModel: AppTrackersViewModel
) : RecyclerView.Adapter<ToggleTrackersAdapter.ViewHolder>() {
-
- var isEnabled = true
-
class ViewHolder(
- view: View,
- private val onToggleSwitch: (Tracker, Boolean) -> Unit,
- private val onClickTitle: (Tracker) -> Unit
- ) : RecyclerView.ViewHolder(view) {
- val title: TextView = view.findViewById(R.id.title)
+ private val binding: ApptrackersItemTrackerToggleBinding,
+ private val viewModel: AppTrackersViewModel,
+ ) : RecyclerView.ViewHolder(binding.root) {
- val toggle: Switch = view.findViewById(R.id.toggle)
+ fun bind(item: Pair<Tracker, Boolean>) {
+ val label = item.first.label
+ with(binding.title) {
+ if (item.first.exodusId != null) {
- fun bind(item: Pair<Tracker, Boolean>, isEnabled: Boolean) {
- val text = item.first.label
- if (item.first.exodusId != null) {
- title.setTextColor(ContextCompat.getColor(title.context, R.color.accent))
- val spannable = SpannableString(text)
- spannable.setSpan(UnderlineSpan(), 0, spannable.length, 0)
- title.text = spannable
- } else {
- title.setTextColor(ContextCompat.getColor(title.context, R.color.primary_text))
- title.text = text
+ setTextColor(ContextCompat.getColor(context, R.color.accent))
+ val spannable = SpannableString(label)
+ spannable.setSpan(UnderlineSpan(), 0, spannable.length, 0)
+ text = spannable
+ } else {
+ setTextColor(ContextCompat.getColor(context, R.color.primary_text))
+ text = label
+ }
+ setOnClickListener { viewModel.onClickTracker(item.first) }
}
+ with(binding.toggle) {
+ isChecked = item.second
- toggle.isChecked = item.second
- toggle.isEnabled = isEnabled
-
- toggle.setOnClickListener {
- onToggleSwitch(item.first, toggle.isChecked)
+ setOnClickListener {
+ viewModel.onToggleTracker(item.first, isChecked)
+ }
}
-
- title.setOnClickListener { onClickTitle(item.first) }
}
}
private var dataSet: List<Pair<Tracker, Boolean>> = emptyList()
- fun updateDataSet(new: List<Pair<Tracker, Boolean>>, isEnabled: Boolean) {
- this.isEnabled = isEnabled
+ fun updateDataSet(new: List<Pair<Tracker, Boolean>>) {
dataSet = new
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val view = LayoutInflater.from(parent.context)
- .inflate(itemsLayout, parent, false)
- return ViewHolder(view, onToggleSwitch, onClickTitle)
+ return ViewHolder(
+ ApptrackersItemTrackerToggleBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ ),
+ viewModel
+ )
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val permission = dataSet[position]
- holder.bind(permission, isEnabled)
+ holder.bind(permission)
}
override fun getItemCount(): Int = dataSet.size
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt
new file mode 100644
index 0000000..d419677
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * 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.advancedprivacy.features.trackers.trackerdetails
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.databinding.ApptrackersItemTrackerToggleBinding
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+
+class TrackerAppsAdapter(
+ private val viewModel: TrackerDetailsViewModel
+) : RecyclerView.Adapter<TrackerAppsAdapter.ViewHolder>() {
+
+ class ViewHolder(
+ private val binding: ApptrackersItemTrackerToggleBinding,
+ private val viewModel: TrackerDetailsViewModel,
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(item: Pair<ApplicationDescription, Boolean>) {
+ val (app, isWhiteListed) = item
+ binding.title.text = app.label
+ binding.toggle.apply {
+ this.isChecked = isWhiteListed
+ setOnClickListener {
+ viewModel.onToggleUnblockApp(app, isChecked)
+ }
+ }
+ }
+ }
+
+ private var dataSet: List<Pair<ApplicationDescription, Boolean>> = emptyList()
+
+ fun updateDataSet(new: List<Pair<ApplicationDescription, Boolean>>) {
+ dataSet = new
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ ApptrackersItemTrackerToggleBinding.inflate(LayoutInflater.from(parent.context), parent, false),
+ viewModel
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val permission = dataSet[position]
+ holder.bind(permission)
+ }
+
+ override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt
new file mode 100644
index 0000000..481c809
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * 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.advancedprivacy.features.trackers.trackerdetails
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.core.content.ContextCompat.getColor
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.navArgs
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.divider.MaterialDividerItemDecoration
+import com.google.android.material.snackbar.Snackbar
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.NavToolbarFragment
+import foundation.e.advancedprivacy.databinding.TrackerdetailsFragmentBinding
+import foundation.e.advancedprivacy.features.trackers.setupDisclaimerBlock
+import kotlinx.coroutines.launch
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+
+class TrackerDetailsFragment : NavToolbarFragment(R.layout.trackerdetails_fragment) {
+
+ private val args: TrackerDetailsFragmentArgs by navArgs()
+ private val viewModel: TrackerDetailsViewModel by viewModel { parametersOf(args.trackerId) }
+
+ private lateinit var binding: TrackerdetailsFragmentBinding
+
+ override fun getTitle(): CharSequence {
+ return ""
+ }
+
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding = TrackerdetailsFragmentBinding.bind(view)
+
+ binding.blockAllToggle.setOnClickListener {
+ viewModel.onToggleBlockAll(binding.blockAllToggle.isChecked)
+ }
+
+ binding.apps.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ setHasFixedSize(true)
+ addItemDecoration(
+ MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL).apply {
+ dividerColor = getColor(requireContext(), R.color.divider)
+ }
+ )
+ adapter = TrackerAppsAdapter(viewModel)
+ }
+
+ setupDisclaimerBlock(binding.disclaimerBlockTrackers.root, viewModel::onClickLearnMore)
+
+ listenViewModel()
+ }
+
+ private fun listenViewModel() {
+ with(viewLifecycleOwner) {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.singleEvents.collect(::handleEvents)
+ }
+ }
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.doOnStartedState()
+ }
+ }
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ render(viewModel.state.value)
+ viewModel.state.collect(::render)
+ }
+ }
+ }
+ }
+
+ private fun handleEvents(event: TrackerDetailsViewModel.SingleEvent) {
+ when (event) {
+ is TrackerDetailsViewModel.SingleEvent.ErrorEvent ->
+ displayToast(getString(event.errorResId))
+ is TrackerDetailsViewModel.SingleEvent.ToastTrackersControlDisabled ->
+ Snackbar.make(
+ binding.root,
+ R.string.apptrackers_tracker_control_disabled_message,
+ Snackbar.LENGTH_LONG
+ ).show()
+ is TrackerDetailsViewModel.SingleEvent.OpenUrl -> {
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, event.url))
+ } catch (e: ActivityNotFoundException) {
+ Toast.makeText(
+ requireContext(),
+ R.string.error_no_activity_view_url,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+
+ private fun render(state: TrackerDetailsState) {
+ setTitle(state.tracker?.label)
+ binding.subtitle.text = getString(R.string.trackerdetails_subtitle, state.tracker?.label)
+ binding.dataAppCount.apply {
+ primaryMessage.setText(R.string.trackerdetails_app_count_primary)
+ number.text = state.detectedCount.toString()
+ secondaryMessage.setText(R.string.trackerdetails_app_count_secondary)
+ }
+
+ binding.dataBlockedLeaks.apply {
+ primaryMessage.setText(R.string.trackerdetails_blocked_leaks_primary)
+ number.text = state.blockedCount.toString()
+ secondaryMessage.text = getString(R.string.trackerdetails_blocked_leaks_secondary, state.leakedCount.toString())
+ }
+
+ binding.blockAllToggle.isChecked = state.isBlockAllActivated
+
+ binding.apps.post {
+ (binding.apps.adapter as TrackerAppsAdapter?)?.updateDataSet(state.appList)
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt
new file mode 100644
index 0000000..9ae7412
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * 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.advancedprivacy.features.trackers.trackerdetails
+
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
+
+data class TrackerDetailsState(
+ val tracker: Tracker? = null,
+ val isBlockAllActivated: Boolean = false,
+ val detectedCount: Int = 0,
+ val blockedCount: Int = 0,
+ val leakedCount: Int = 0,
+ val appList: List<Pair<ApplicationDescription, Boolean>> = emptyList(),
+ val isTrackersBlockingEnabled: Boolean = false,
+)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt
new file mode 100644
index 0000000..91a1f2a
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * 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.advancedprivacy.features.trackers.trackerdetails
+
+import android.net.Uri
+import androidx.annotation.StringRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.domain.entities.TrackerMode
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackerDetailsUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.advancedprivacy.features.trackers.URL_LEARN_MORE_ABOUT_TRACKERS
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class TrackerDetailsViewModel(
+ private val tracker: Tracker,
+ private val trackersStateUseCase: TrackersStateUseCase,
+ private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
+ private val trackerDetailsUseCase: TrackerDetailsUseCase,
+ private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase
+) : ViewModel() {
+ private val _state = MutableStateFlow(TrackerDetailsState(tracker = tracker))
+ val state = _state.asStateFlow()
+
+ private val _singleEvents = MutableSharedFlow<SingleEvent>()
+ val singleEvents = _singleEvents.asSharedFlow()
+
+ suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+ merge(
+ getQuickPrivacyStateUseCase.trackerMode.map {
+ _state.update { s -> s.copy(isTrackersBlockingEnabled = it != TrackerMode.VULNERABLE) }
+ },
+ trackersStatisticsUseCase.listenUpdates().map { fetchStatistics() }
+ ).collect { }
+ }
+
+ fun onToggleUnblockApp(app: ApplicationDescription, isBlocked: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
+ if (!state.value.isTrackersBlockingEnabled) {
+ _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
+ }
+
+ trackersStateUseCase.blockTracker(app, tracker, isBlocked)
+ updateWhitelist()
+ }
+ }
+
+ fun onToggleBlockAll(isBlocked: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
+ if (!state.value.isTrackersBlockingEnabled) {
+ _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
+ }
+ trackerDetailsUseCase.toggleTrackerWhitelist(tracker, isBlocked)
+ _state.update {
+ it.copy(
+ isBlockAllActivated = !trackersStateUseCase.isWhitelisted(tracker)
+ )
+ }
+ updateWhitelist()
+ }
+ }
+
+ fun onClickLearnMore() {
+ viewModelScope.launch {
+ _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
+ }
+ }
+
+ private suspend fun fetchStatistics() = withContext(Dispatchers.IO) {
+ val (blocked, leaked) = trackerDetailsUseCase.getCalls(tracker)
+ val appsWhitWhiteListState = trackerDetailsUseCase.getAppsWithBlockedState(tracker)
+
+ _state.update { s ->
+ s.copy(
+ isBlockAllActivated = !trackersStateUseCase.isWhitelisted(tracker),
+ detectedCount = appsWhitWhiteListState.size,
+ blockedCount = blocked,
+ leakedCount = leaked,
+ appList = appsWhitWhiteListState,
+ )
+ }
+ }
+
+ private suspend fun updateWhitelist() {
+ _state.update { s ->
+ s.copy(
+ isBlockAllActivated = !trackersStateUseCase.isWhitelisted(tracker),
+ appList = trackerDetailsUseCase.enrichWithBlockedState(
+ s.appList.map { it.first }, tracker
+ )
+ )
+ }
+ }
+
+ sealed class SingleEvent {
+ data class ErrorEvent(@StringRes val errorResId: Int) : SingleEvent()
+ object ToastTrackersControlDisabled : SingleEvent()
+ data class OpenUrl(val url: Uri) : SingleEvent()
+ }
+}