aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/foundation/e/privacycentralapp/features
diff options
context:
space:
mode:
authorGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-05-02 21:25:17 +0200
committerGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-05-02 22:00:35 +0200
commita8874167f663885f2d3371801cf03681576ac817 (patch)
tree5be07b8768142efeade536d4135f2250c1ac9071 /app/src/main/java/foundation/e/privacycentralapp/features
parenta0ee04ea9dbc0802c828afdf660eb37dc6fa350f (diff)
downloadadvanced-privacy-a8874167f663885f2d3371801cf03681576ac817.tar.gz
1200: rename everything to AdvancedPrivacy
Diffstat (limited to 'app/src/main/java/foundation/e/privacycentralapp/features')
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt307
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt37
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt158
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt201
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt36
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt157
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt376
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt53
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt29
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt126
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt218
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt28
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt95
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt189
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt42
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt172
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt92
17 files changed, 0 insertions, 2316 deletions
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
deleted file mode 100644
index 0dc24e8..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt
+++ /dev/null
@@ -1,307 +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.dashboard
-
-import android.content.Intent
-import android.os.Bundle
-import android.text.Html
-import android.text.Html.FROM_HTML_MODE_LEGACY
-import android.view.View
-import android.widget.Toast
-import androidx.core.content.ContextCompat.getColor
-import androidx.core.os.bundleOf
-import androidx.core.view.isVisible
-import androidx.fragment.app.commit
-import androidx.fragment.app.replace
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import foundation.e.privacycentralapp.DependencyContainer
-import foundation.e.privacycentralapp.PrivacyCentralApplication
-import foundation.e.privacycentralapp.R
-import foundation.e.privacycentralapp.common.GraphHolder
-import foundation.e.privacycentralapp.common.NavToolbarFragment
-import foundation.e.privacycentralapp.databinding.FragmentDashboardBinding
-import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode
-import foundation.e.privacycentralapp.domain.entities.LocationMode
-import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState
-import foundation.e.privacycentralapp.domain.entities.TrackerMode
-import foundation.e.privacycentralapp.features.dashboard.DashboardViewModel.Action
-import foundation.e.privacycentralapp.features.dashboard.DashboardViewModel.SingleEvent
-import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyFragment
-import foundation.e.privacycentralapp.features.location.FakeLocationFragment
-import foundation.e.privacycentralapp.features.trackers.TrackersFragment
-import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFragment
-import kotlinx.coroutines.launch
-
-class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) {
- companion object {
- private const val PARAM_HIGHLIGHT_INDEX = "PARAM_HIGHLIGHT_INDEX"
- fun buildArgs(highlightIndex: Int): Bundle = bundleOf(
- PARAM_HIGHLIGHT_INDEX to highlightIndex
- )
- }
-
- private val dependencyContainer: DependencyContainer by lazy {
- (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer
- }
-
- private val viewModel: DashboardViewModel by viewModels {
- dependencyContainer.viewModelsFactory
- }
-
- private var graphHolder: GraphHolder? = null
-
- private var _binding: FragmentDashboardBinding? = null
- private val binding get() = _binding!!
-
- private var highlightIndexOnStart: Int? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- highlightIndexOnStart = arguments?.getInt(PARAM_HIGHLIGHT_INDEX, -1)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- _binding = FragmentDashboardBinding.bind(view)
-
- graphHolder = GraphHolder(binding.graph, requireContext())
-
- binding.leakingAppButton.setOnClickListener {
- viewModel.submitAction(Action.ShowMostLeakedApp)
- }
- binding.toggleTrackers.setOnClickListener {
- viewModel.submitAction(Action.ToggleTrackers)
- }
- binding.toggleLocation.setOnClickListener {
- viewModel.submitAction(Action.ToggleLocation)
- }
- binding.toggleIpscrambling.setOnClickListener {
- viewModel.submitAction(Action.ToggleIpScrambling)
- }
- binding.myLocation.container.setOnClickListener {
- viewModel.submitAction(Action.ShowFakeMyLocationAction)
- }
- binding.internetActivityPrivacy.container.setOnClickListener {
- viewModel.submitAction(Action.ShowInternetActivityPrivacyAction)
- }
- binding.appsPermissions.container.setOnClickListener {
- viewModel.submitAction(Action.ShowAppsPermissions)
- }
-
- binding.amITracked.container.setOnClickListener {
- viewModel.submitAction(Action.ShowTrackers)
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- render(viewModel.state.value)
- viewModel.state.collect(::render)
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.singleEvents.collect { event ->
- when (event) {
- is SingleEvent.NavigateToLocationSingleEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<FakeLocationFragment>(R.id.container)
- setReorderingAllowed(true)
- addToBackStack("dashboard")
- }
- }
- is SingleEvent.NavigateToInternetActivityPrivacySingleEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<InternetPrivacyFragment>(R.id.container)
- setReorderingAllowed(true)
- addToBackStack("dashboard")
- }
- }
- is SingleEvent.NavigateToPermissionsSingleEvent -> {
- val intent = Intent("android.intent.action.MANAGE_PERMISSIONS")
- requireActivity().startActivity(intent)
- }
- SingleEvent.NavigateToTrackersSingleEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<TrackersFragment>(R.id.container)
- setReorderingAllowed(true)
- addToBackStack("dashboard")
- }
- }
- is SingleEvent.NavigateToAppDetailsEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<AppTrackersFragment>(
- R.id.container,
- args = AppTrackersFragment.buildArgs(
- event.appDesc.label.toString(),
- event.appDesc.packageName,
- event.appDesc.uid
- )
- )
- setReorderingAllowed(true)
- addToBackStack("dashboard")
- }
- }
- is SingleEvent.ToastMessageSingleEvent ->
- Toast.makeText(
- requireContext(),
- getString(event.message, *event.args.toTypedArray()),
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.doOnStartedState()
- }
- }
- }
-
- override fun getTitle(): String {
- return getString(R.string.dashboard_title)
- }
-
- private fun render(state: DashboardState) {
- binding.stateLabel.text = getString(
- when (state.quickPrivacyState) {
- QuickPrivacyState.DISABLED -> R.string.dashboard_state_title_off
- QuickPrivacyState.FULL_ENABLED -> R.string.dashboard_state_title_on
- QuickPrivacyState.ENABLED -> R.string.dashboard_state_title_custom
- }
- )
-
- binding.stateIcon.setImageResource(
- if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on
- else R.drawable.ic_shield_off
- )
-
- binding.toggleTrackers.isChecked = state.trackerMode != TrackerMode.VULNERABLE
-
- binding.stateTrackers.text = getString(
- when (state.trackerMode) {
- TrackerMode.DENIED -> R.string.dashboard_state_trackers_on
- TrackerMode.VULNERABLE -> R.string.dashboard_state_trackers_off
- TrackerMode.CUSTOM -> R.string.dashboard_state_trackers_custom
- }
- )
- binding.stateTrackers.setTextColor(
- getColor(
- requireContext(),
- if (state.trackerMode == TrackerMode.VULNERABLE) R.color.red_off
- else R.color.green_valid
- )
- )
-
- binding.toggleLocation.isChecked = state.isLocationHidden
-
- binding.stateGeolocation.text = getString(
- if (state.isLocationHidden) R.string.dashboard_state_geolocation_on
- else R.string.dashboard_state_geolocation_off
- )
- binding.stateGeolocation.setTextColor(
- getColor(
- requireContext(),
- if (state.isLocationHidden) R.color.green_valid
- else R.color.red_off
- )
- )
-
- binding.toggleIpscrambling.isChecked = state.ipScramblingMode.isChecked
- val isLoading = state.ipScramblingMode.isLoading
-
- binding.stateIpAddress.text = getString(
- if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.dashboard_state_ipaddress_on
- else R.string.dashboard_state_ipaddress_off
- )
-
- binding.stateIpAddressLoader.visibility = if (isLoading) View.VISIBLE else View.GONE
- binding.stateIpAddress.visibility = if (!isLoading) View.VISIBLE else View.GONE
-
- binding.stateIpAddress.setTextColor(
- getColor(
- requireContext(),
- if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.color.green_valid
- else R.color.red_off
- )
- )
-
- if (state.dayStatistics?.all { it.first == 0 && it.second == 0 } == true) {
- binding.graph.visibility = View.INVISIBLE
- binding.graphLegend.isVisible = false
- binding.leakingAppButton.isVisible = false
- binding.graphEmpty.isVisible = true
- } else {
- binding.graph.isVisible = true
- binding.graphLegend.isVisible = true
- binding.leakingAppButton.isVisible = true
- binding.graphEmpty.isVisible = false
- state.dayStatistics?.let { graphHolder?.data = it }
- state.dayLabels?.let { graphHolder?.labels = it }
- state.dayGraduations?.let { graphHolder?.graduations = it }
-
- binding.graphLegend.text = Html.fromHtml(
- getString(
- R.string.dashboard_graph_trackers_legend,
- state.leakedTrackersCount?.toString() ?: "No"
- ),
- FROM_HTML_MODE_LEGACY
- )
-
- highlightIndexOnStart?.let {
- binding.graph.post {
- graphHolder?.highlightIndex(it)
- }
- highlightIndexOnStart = null
- }
- }
-
- if (state.allowedTrackersCount != null && state.trackersCount != null) {
- binding.amITracked.subTitle = getString(R.string.dashboard_am_i_tracked_subtitle, state.trackersCount, state.allowedTrackersCount)
- } else {
- binding.amITracked.subTitle = ""
- }
-
- binding.myLocation.subTitle = getString(
- when (state.locationMode) {
- LocationMode.REAL_LOCATION -> R.string.dashboard_location_subtitle_off
- LocationMode.SPECIFIC_LOCATION -> R.string.dashboard_location_subtitle_specific
- LocationMode.RANDOM_LOCATION -> R.string.dashboard_location_subtitle_random
- }
- )
-
- binding.internetActivityPrivacy.subTitle = getString(
- if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.dashboard_internet_activity_privacy_subtitle_on
- else R.string.dashboard_internet_activity_privacy_subtitle_off
- )
-
- binding.executePendingBindings()
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- graphHolder = null
- _binding = null
- }
-}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt
deleted file mode 100644
index 0e3521d..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2022 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.dashboard
-
-import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode
-import foundation.e.privacycentralapp.domain.entities.LocationMode
-import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState
-import foundation.e.privacycentralapp.domain.entities.TrackerMode
-
-data class DashboardState(
- val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED,
- val trackerMode: TrackerMode = TrackerMode.VULNERABLE,
- val isLocationHidden: Boolean = false,
- val ipScramblingMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP_LOADING,
- val locationMode: LocationMode = LocationMode.REAL_LOCATION,
- val leakedTrackersCount: Int? = null,
- val trackersCount: Int? = null,
- val allowedTrackersCount: Int? = null,
- val dayStatistics: List<Pair<Int, Int>>? = null,
- val dayLabels: List<String>? = null,
- val dayGraduations: List<String?>? = null,
-)
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt
deleted file mode 100644
index f3a9774..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
-* Copyright (C) 2023 MURENA SAS
- * 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.dashboard
-
-import androidx.annotation.StringRes
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.R
-import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase
-import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase
-import foundation.e.privacymodules.permissions.data.ApplicationDescription
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class DashboardViewModel(
- private val getPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
-) : ViewModel() {
-
- private val _state = MutableStateFlow(DashboardState())
- val state = _state.asStateFlow()
-
- private val _singleEvents = MutableSharedFlow<SingleEvent>()
- val singleEvents = _singleEvents.asSharedFlow()
-
- init {
- viewModelScope.launch(Dispatchers.IO) { trackersStatisticsUseCase.initAppList() }
- }
-
- suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
- merge(
- getPrivacyStateUseCase.quickPrivacyState.map {
- _state.update { s -> s.copy(quickPrivacyState = it) }
- },
- getPrivacyStateUseCase.ipScramblingMode.map {
- _state.update { s -> s.copy(ipScramblingMode = it) }
- },
- trackersStatisticsUseCase.listenUpdates().flatMapLatest {
- fetchStatistics()
- },
- getPrivacyStateUseCase.trackerMode.map {
- _state.update { s -> s.copy(trackerMode = it) }
- },
- getPrivacyStateUseCase.isLocationHidden.map {
- _state.update { s -> s.copy(isLocationHidden = it) }
- },
- getPrivacyStateUseCase.locationMode.map {
- _state.update { s -> s.copy(locationMode = it) }
- },
- getPrivacyStateUseCase.otherVpnRunning.map {
- _singleEvents.emit(
- SingleEvent.ToastMessageSingleEvent(
- R.string.ipscrambling_error_always_on_vpn_already_running,
- listOf(it.label ?: "")
- )
- )
- }
- ).collect {}
- }
-
- fun submitAction(action: Action) = viewModelScope.launch {
- when (action) {
- is Action.ToggleTrackers -> {
- getPrivacyStateUseCase.toggleTrackers()
- // Add delay here to prevent race condition with trackers state.
- delay(200)
- fetchStatistics().first()
- }
- is Action.ToggleLocation -> getPrivacyStateUseCase.toggleLocation()
- is Action.ToggleIpScrambling -> getPrivacyStateUseCase.toggleIpScrambling()
- is Action.ShowFakeMyLocationAction ->
- _singleEvents.emit(SingleEvent.NavigateToLocationSingleEvent)
- is Action.ShowAppsPermissions ->
- _singleEvents.emit(SingleEvent.NavigateToPermissionsSingleEvent)
- is Action.ShowInternetActivityPrivacyAction ->
- _singleEvents.emit(SingleEvent.NavigateToInternetActivityPrivacySingleEvent)
- is Action.ShowTrackers ->
- _singleEvents.emit(SingleEvent.NavigateToTrackersSingleEvent)
- is Action.ShowMostLeakedApp -> actionShowMostLeakedApp()
- }
- }
-
- private suspend fun fetchStatistics(): Flow<Unit> = withContext(Dispatchers.IO) {
- trackersStatisticsUseCase.getNonBlockedTrackersCount().map { nonBlockedTrackersCount ->
- trackersStatisticsUseCase.getDayStatistics().let { (dayStatistics, trackersCount) ->
- _state.update { s ->
- s.copy(
- dayStatistics = dayStatistics.callsBlockedNLeaked,
- dayLabels = dayStatistics.periods,
- dayGraduations = dayStatistics.graduations,
- leakedTrackersCount = dayStatistics.trackersCount,
- trackersCount = trackersCount,
- allowedTrackersCount = nonBlockedTrackersCount
- )
- }
- }
- }
- }
-
- private suspend fun actionShowMostLeakedApp() = withContext(Dispatchers.IO) {
- _singleEvents.emit(
- trackersStatisticsUseCase.getMostLeakedApp()?.let {
- SingleEvent.NavigateToAppDetailsEvent(appDesc = it)
- } ?: SingleEvent.NavigateToTrackersSingleEvent
- )
- }
-
- sealed class SingleEvent {
- object NavigateToTrackersSingleEvent : SingleEvent()
- object NavigateToInternetActivityPrivacySingleEvent : SingleEvent()
- object NavigateToLocationSingleEvent : SingleEvent()
- object NavigateToPermissionsSingleEvent : SingleEvent()
- data class NavigateToAppDetailsEvent(val appDesc: ApplicationDescription) : SingleEvent()
- data class ToastMessageSingleEvent(
- @StringRes val message: Int,
- val args: List<Any> = emptyList()
- ) : SingleEvent()
- }
-
- sealed class Action {
- object ToggleTrackers : Action()
- object ToggleLocation : Action()
- object ToggleIpScrambling : Action()
- object ShowFakeMyLocationAction : Action()
- object ShowInternetActivityPrivacyAction : Action()
- object ShowAppsPermissions : Action()
- object ShowTrackers : Action()
- object ShowMostLeakedApp : Action()
- }
-}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
deleted file mode 100644
index afef986..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
+++ /dev/null
@@ -1,201 +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.internetprivacy
-
-import android.os.Bundle
-import android.view.View
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.LinearLayoutManager
-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.common.ToggleAppsAdapter
-import foundation.e.privacycentralapp.common.setToolTipForAsterisk
-import foundation.e.privacycentralapp.databinding.FragmentInternetActivityPolicyBinding
-import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode
-import kotlinx.coroutines.launch
-import java.util.Locale
-
-class InternetPrivacyFragment : NavToolbarFragment(R.layout.fragment_internet_activity_policy) {
-
- private val dependencyContainer: DependencyContainer by lazy {
- (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer
- }
-
- private val viewModel: InternetPrivacyViewModel by viewModels {
- dependencyContainer.viewModelsFactory
- }
-
- private var _binding: FragmentInternetActivityPolicyBinding? = null
- private val binding get() = _binding!!
-
- 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 = FragmentInternetActivityPolicyBinding.bind(view)
-
- binding.apps.apply {
- layoutManager = LinearLayoutManager(requireContext())
- setHasFixedSize(true)
- adapter = ToggleAppsAdapter(R.layout.ipscrambling_item_app_toggle) { packageName ->
- viewModel.submitAction(
- InternetPrivacyViewModel.Action.ToggleAppIpScrambled(packageName)
- )
- }
- }
-
- binding.radioUseRealIp.radiobutton.setOnClickListener {
- viewModel.submitAction(InternetPrivacyViewModel.Action.UseRealIPAction)
- }
-
- binding.radioUseHiddenIp.radiobutton.setOnClickListener {
- viewModel.submitAction(InternetPrivacyViewModel.Action.UseHiddenIPAction)
- }
-
- setToolTipForAsterisk(
- textView = binding.ipscramblingSelectApps,
- textId = R.string.ipscrambling_select_app,
- tooltipTextId = R.string.ipscrambling_app_list_infos
- )
-
- binding.ipscramblingSelectLocation.apply {
- adapter = ArrayAdapter(
- requireContext(), android.R.layout.simple_spinner_item,
- viewModel.availablesLocationsIds.map {
- if (it == "") {
- getString(R.string.ipscrambling_any_location)
- } else {
- Locale("", it).displayCountry
- }
- }
- ).apply {
- setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
- }
-
- onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onItemSelected(
- parentView: AdapterView<*>,
- selectedItemView: View?,
- position: Int,
- id: Long
- ) {
- viewModel.submitAction(
- InternetPrivacyViewModel.Action.SelectLocationAction(
- position
- )
- )
- }
-
- override fun onNothingSelected(parentView: AdapterView<*>?) {}
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- render(viewModel.state.value)
- viewModel.state.collect(::render)
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.singleEvents.collect { event ->
- when (event) {
- is InternetPrivacyViewModel.SingleEvent.ErrorEvent -> {
- displayToast(getString(event.errorResId, *event.args.toTypedArray()))
- }
- }
- }
- }
- }
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.doOnStartedState()
- }
- }
- }
-
- override fun getTitle(): String = getString(R.string.ipscrambling_title)
-
- private fun render(state: InternetPrivacyState) {
- binding.radioUseHiddenIp.radiobutton.apply {
- isChecked = state.mode in listOf(
- InternetPrivacyMode.HIDE_IP,
- InternetPrivacyMode.HIDE_IP_LOADING
- )
- isEnabled = state.mode != InternetPrivacyMode.HIDE_IP_LOADING
- }
- binding.radioUseRealIp.radiobutton.apply {
- isChecked =
- state.mode in listOf(
- InternetPrivacyMode.REAL_IP,
- InternetPrivacyMode.REAL_IP_LOADING
- )
- isEnabled = state.mode != InternetPrivacyMode.REAL_IP_LOADING
- }
-
- binding.ipscramblingSelectLocation.setSelection(state.selectedLocationPosition)
-
- // TODO: this should not be mandatory.
- binding.apps.post {
- (binding.apps.adapter as ToggleAppsAdapter?)?.setData(
- list = state.getApps(),
- isEnabled = state.mode == InternetPrivacyMode.HIDE_IP
- )
- }
-
- val viewIdsToHide = listOf(
- binding.ipscramblingLocationLabel,
- binding.selectLocationContainer,
- binding.ipscramblingSelectLocation,
- binding.ipscramblingSelectApps,
- binding.apps
- )
-
- when {
- state.mode in listOf(
- InternetPrivacyMode.HIDE_IP_LOADING,
- InternetPrivacyMode.REAL_IP_LOADING
- )
- || state.availableApps.isEmpty() -> {
- binding.loader.visibility = View.VISIBLE
- viewIdsToHide.forEach { it.visibility = View.GONE }
- }
- else -> {
- binding.loader.visibility = View.GONE
- viewIdsToHide.forEach { it.visibility = View.VISIBLE }
- }
- }
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt
deleted file mode 100644
index 54b7e01..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2022 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.internetprivacy
-
-import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode
-import foundation.e.privacymodules.permissions.data.ApplicationDescription
-
-data class InternetPrivacyState(
- val mode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP,
- val availableApps: List<ApplicationDescription> = emptyList(),
- val bypassTorApps: Collection<String> = emptyList(),
- val selectedLocation: String = "",
- val availableLocationIds: List<String> = emptyList(),
- val forceRedraw: Boolean = false,
-) {
- fun getApps(): List<Pair<ApplicationDescription, Boolean>> {
- return availableApps.map { it to (it.packageName !in bypassTorApps) }
- }
-
- val selectedLocationPosition get() = availableLocationIds.indexOf(selectedLocation)
-}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
deleted file mode 100644
index bbd6239..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
+++ /dev/null
@@ -1,157 +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.internetprivacy
-
-import androidx.annotation.StringRes
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.R
-import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode
-import foundation.e.privacycentralapp.domain.usecases.AppListUseCase
-import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase
-import foundation.e.privacycentralapp.domain.usecases.IpScramblingStateUseCase
-import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class InternetPrivacyViewModel(
- private val ipScramblerModule: IIpScramblerModule,
- private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- private val ipScramblingStateUseCase: IpScramblingStateUseCase,
- private val appListUseCase: AppListUseCase
-) : ViewModel() {
- companion object {
- private const val WARNING_LOADING_LONG_DELAY = 5 * 1000L
- }
-
- private val _state = MutableStateFlow(InternetPrivacyState())
- val state = _state.asStateFlow()
-
- private val _singleEvents = MutableSharedFlow<SingleEvent>()
- val singleEvents = _singleEvents.asSharedFlow()
-
- val availablesLocationsIds = listOf("", *ipScramblerModule.getAvailablesLocations().sorted().toTypedArray())
-
- init {
- viewModelScope.launch(Dispatchers.IO) {
- _state.update {
- it.copy(
- mode = ipScramblingStateUseCase.internetPrivacyMode.value,
- availableLocationIds = availablesLocationsIds,
- selectedLocation = ipScramblerModule.exitCountry
- )
- }
- }
- }
-
- @OptIn(FlowPreview::class)
- suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
- launch {
- merge(
- appListUseCase.getAppsUsingInternet().map { apps ->
- _state.update { s ->
- s.copy(
- availableApps = apps,
- bypassTorApps = ipScramblingStateUseCase.bypassTorApps
- )
- }
- },
- ipScramblingStateUseCase.internetPrivacyMode.map {
- _state.update { s -> s.copy(mode = it) }
- }
- ).collect {}
- }
-
- launch {
- ipScramblingStateUseCase.internetPrivacyMode
- .map { it == InternetPrivacyMode.HIDE_IP_LOADING }
- .debounce(WARNING_LOADING_LONG_DELAY)
- .collect {
- if (it) _singleEvents.emit(
- SingleEvent.ErrorEvent(R.string.ipscrambling_warning_starting_long)
- )
- }
- }
-
- launch {
- getQuickPrivacyStateUseCase.otherVpnRunning.collect {
- _singleEvents.emit(
- SingleEvent.ErrorEvent(
- R.string.ipscrambling_error_always_on_vpn_already_running,
- listOf(it.label ?: "")
- )
- )
- _state.update { it.copy(forceRedraw = !it.forceRedraw) }
- }
- }
- }
-
- fun submitAction(action: Action) = viewModelScope.launch {
- when (action) {
- is Action.UseRealIPAction -> actionUseRealIP()
- is Action.UseHiddenIPAction -> actionUseHiddenIP()
- is Action.ToggleAppIpScrambled -> actionToggleAppIpScrambled(action)
- is Action.SelectLocationAction -> actionSelectLocation(action)
- }
- }
-
- private fun actionUseRealIP() {
- ipScramblingStateUseCase.toggle(hideIp = false)
- }
-
- private fun actionUseHiddenIP() {
- ipScramblingStateUseCase.toggle(hideIp = true)
- }
-
- private suspend fun actionToggleAppIpScrambled(action: Action.ToggleAppIpScrambled) = withContext(Dispatchers.IO) {
- ipScramblingStateUseCase.toggleBypassTor(action.packageName)
- _state.update { it.copy(bypassTorApps = ipScramblingStateUseCase.bypassTorApps) }
- }
-
- private suspend fun actionSelectLocation(action: Action.SelectLocationAction) = withContext(Dispatchers.IO) {
- val locationId = _state.value.availableLocationIds[action.position]
- if (locationId != ipScramblerModule.exitCountry) {
- ipScramblerModule.exitCountry = locationId
- _state.update { it.copy(selectedLocation = locationId) }
- }
- }
-
- sealed class SingleEvent {
- data class ErrorEvent(
- @StringRes val errorResId: Int,
- val args: List<Any> = emptyList()
- ) : SingleEvent()
- }
-
- sealed class Action {
- object UseRealIPAction : Action()
- object UseHiddenIPAction : Action()
- data class ToggleAppIpScrambled(val packageName: String) : Action()
- data class SelectLocationAction(val position: Int) : Action()
- }
-}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt
deleted file mode 100644
index 9e3f854..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt
+++ /dev/null
@@ -1,376 +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.location
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.content.Context
-import android.location.Location
-import android.os.Bundle
-import android.text.Editable
-import android.view.View
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.NonNull
-import androidx.core.view.isVisible
-import androidx.core.widget.addTextChangedListener
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.google.android.material.textfield.TextInputEditText
-import com.google.android.material.textfield.TextInputLayout
-import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM
-import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
-import com.mapbox.mapboxsdk.Mapbox
-import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
-import com.mapbox.mapboxsdk.geometry.LatLng
-import com.mapbox.mapboxsdk.location.LocationComponent
-import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions
-import com.mapbox.mapboxsdk.location.LocationUpdate
-import com.mapbox.mapboxsdk.location.modes.CameraMode
-import com.mapbox.mapboxsdk.location.modes.RenderMode
-import com.mapbox.mapboxsdk.maps.MapboxMap
-import com.mapbox.mapboxsdk.maps.Style
-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.FragmentFakeLocationBinding
-import foundation.e.privacycentralapp.domain.entities.LocationMode
-import foundation.e.privacycentralapp.features.location.FakeLocationViewModel.Action
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.ensureActive
-import kotlinx.coroutines.launch
-
-class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) {
-
- private var isFirstLaunch: Boolean = true
-
- private val dependencyContainer: DependencyContainer by lazy {
- (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer
- }
-
- private val viewModel: FakeLocationViewModel by viewModels {
- dependencyContainer.viewModelsFactory
- }
-
- private var _binding: FragmentFakeLocationBinding? = null
- private val binding get() = _binding!!
-
- private var mapboxMap: MapboxMap? = null
- private var locationComponent: LocationComponent? = null
-
- private var inputJob: Job? = null
-
- private val locationPermissionRequest = registerForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions()
- ) { permissions ->
- if (permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) ||
- permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false)
- ) {
- viewModel.submitAction(Action.StartListeningLocation)
- } // TODO: else.
- }
-
- companion object {
- private const val DEBOUNCE_PERIOD = 1000L
- }
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- Mapbox.getInstance(requireContext(), getString(R.string.mapbox_key))
- }
-
- override fun getTitle(): String = getString(R.string.location_title)
-
- 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 = FragmentFakeLocationBinding.bind(view)
-
- binding.mapView.setup(savedInstanceState) { mapboxMap ->
- this.mapboxMap = mapboxMap
- mapboxMap.uiSettings.isRotateGesturesEnabled = false
- mapboxMap.setStyle(Style.MAPBOX_STREETS) { style ->
- enableLocationPlugin(style)
-
- mapboxMap.addOnCameraMoveListener {
- if (binding.mapView.isEnabled) {
- mapboxMap.cameraPosition.target.let {
- viewModel.submitAction(
- Action.SetSpecificLocationAction(
- it.latitude.toFloat(),
- it.longitude.toFloat()
- )
- )
- }
- }
- }
- // Bind click listeners once map is ready.
- bindClickListeners()
-
- render(viewModel.state.value)
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.singleEvents.collect { event ->
- if (event is FakeLocationViewModel.SingleEvent.LocationUpdatedEvent) {
- updateLocation(event.location, event.mode)
- }
- }
- }
- }
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- render(viewModel.state.value)
- viewModel.state.collect(::render)
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.singleEvents.collect { event ->
- when (event) {
- is FakeLocationViewModel.SingleEvent.ErrorEvent -> {
- displayToast(event.error)
- }
- is FakeLocationViewModel.SingleEvent.RequestLocationPermission -> {
- // TODO for standalone: rationale dialog
- locationPermissionRequest.launch(
- arrayOf(
- Manifest.permission.ACCESS_FINE_LOCATION,
- Manifest.permission.ACCESS_COARSE_LOCATION
- )
- )
- }
- is FakeLocationViewModel.SingleEvent.LocationUpdatedEvent -> {
- // Nothing here, another collect linked to mapbox view.
- }
- }
- }
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.doOnStartedState()
- }
- }
- }
-
- private fun getCoordinatesAfterTextChanged(
- inputLayout: TextInputLayout,
- editText: TextInputEditText,
- isLat: Boolean
- ) = { editable: Editable? ->
- inputJob?.cancel()
- if (editable != null && editable.isNotEmpty() && editText.isEnabled) {
- inputJob = lifecycleScope.launch {
- delay(DEBOUNCE_PERIOD)
- ensureActive()
- try {
- val value = editable.toString().toFloat()
- val maxValue = if (isLat) 90f else 180f
-
- if (value > maxValue || value < -maxValue) {
- throw NumberFormatException("value $value is out of bounds")
- }
- inputLayout.error = null
-
- inputLayout.setEndIconDrawable(R.drawable.ic_valid)
- inputLayout.endIconMode = END_ICON_CUSTOM
-
- // Here, value is valid, try to send the values
- try {
- val lat = binding.edittextLatitude.text.toString().toFloat()
- val lon = binding.edittextLongitude.text.toString().toFloat()
- if (lat <= 90f && lat >= -90f && lon <= 180f && lon >= -180f) {
- mapboxMap?.moveCamera(
- CameraUpdateFactory.newLatLng(
- LatLng(lat.toDouble(), lon.toDouble())
- )
- )
- }
- } catch (e: NumberFormatException) {
- }
- } catch (e: NumberFormatException) {
- inputLayout.endIconMode = END_ICON_NONE
- inputLayout.error = getString(R.string.location_input_error)
- }
- }
- }
- }
-
- @SuppressLint("ClickableViewAccessibility")
- private fun bindClickListeners() {
- binding.radioUseRealLocation.setOnClickListener {
- viewModel.submitAction(Action.UseRealLocationAction)
- }
- binding.radioUseRandomLocation.setOnClickListener {
- viewModel.submitAction(Action.UseRandomLocationAction)
- }
- binding.radioUseSpecificLocation.setOnClickListener {
- mapboxMap?.cameraPosition?.target?.let {
- viewModel.submitAction(
- Action.SetSpecificLocationAction(it.latitude.toFloat(), it.longitude.toFloat())
- )
- }
- }
- binding.edittextLatitude.addTextChangedListener(
- afterTextChanged = getCoordinatesAfterTextChanged(
- binding.textlayoutLatitude,
- binding.edittextLatitude,
- true
- )
- )
-
- binding.edittextLongitude.addTextChangedListener(
- afterTextChanged = getCoordinatesAfterTextChanged(
- binding.textlayoutLongitude,
- binding.edittextLongitude,
- false
- )
- )
- }
-
- @SuppressLint("MissingPermission")
- private fun render(state: FakeLocationState) {
- binding.radioUseRandomLocation.isChecked = state.mode == LocationMode.RANDOM_LOCATION
-
- binding.radioUseSpecificLocation.isChecked = state.mode == LocationMode.SPECIFIC_LOCATION
-
- binding.radioUseRealLocation.isChecked = state.mode == LocationMode.REAL_LOCATION
-
- binding.mapView.isEnabled = (state.mode == LocationMode.SPECIFIC_LOCATION)
-
- if (state.mode == LocationMode.REAL_LOCATION) {
- binding.centeredMarker.isVisible = false
- } else {
- binding.mapLoader.isVisible = false
- binding.mapOverlay.isVisible = state.mode != LocationMode.SPECIFIC_LOCATION
- binding.centeredMarker.isVisible = true
-
- mapboxMap?.moveCamera(
- CameraUpdateFactory.newLatLng(
- LatLng(
- state.specificLatitude?.toDouble() ?: 0.0,
- state.specificLongitude?.toDouble() ?: 0.0
- )
- )
- )
- }
-
- binding.textlayoutLatitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION)
- binding.textlayoutLongitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION)
-
- binding.edittextLatitude.setText(state.specificLatitude?.toString())
- binding.edittextLongitude.setText(state.specificLongitude?.toString())
- }
-
- @SuppressLint("MissingPermission")
- private fun updateLocation(lastLocation: Location?, mode: LocationMode) {
- lastLocation?.let { location ->
- locationComponent?.isLocationComponentEnabled = true
- val locationUpdate = LocationUpdate.Builder()
- .location(location)
- .animationDuration(100)
- .build()
- locationComponent?.forceLocationUpdate(locationUpdate)
-
- if (mode == LocationMode.REAL_LOCATION) {
- binding.mapLoader.isVisible = false
- binding.mapOverlay.isVisible = false
-
- val update = CameraUpdateFactory.newLatLng(
- LatLng(location.latitude, location.longitude)
- )
-
- if (isFirstLaunch) {
- mapboxMap?.moveCamera(update)
- isFirstLaunch = false
- } else {
- mapboxMap?.animateCamera(update)
- }
- }
- } ?: run {
- locationComponent?.isLocationComponentEnabled = false
- if (mode == LocationMode.REAL_LOCATION) {
- binding.mapLoader.isVisible = true
- binding.mapOverlay.isVisible = true
- }
- }
- }
-
- @SuppressLint("MissingPermission")
- private fun enableLocationPlugin(@NonNull loadedMapStyle: Style) {
- // Check if permissions are enabled and if not request
- locationComponent = mapboxMap?.locationComponent
- locationComponent?.activateLocationComponent(
- LocationComponentActivationOptions.builder(
- requireContext(), loadedMapStyle
- ).useDefaultLocationEngine(false).build()
- )
- locationComponent?.isLocationComponentEnabled = true
- locationComponent?.cameraMode = CameraMode.NONE
- locationComponent?.renderMode = RenderMode.NORMAL
- }
-
- override fun onStart() {
- super.onStart()
- binding.mapView.onStart()
- }
-
- override fun onResume() {
- super.onResume()
- viewModel.submitAction(Action.StartListeningLocation)
- binding.mapView.onResume()
- }
-
- override fun onPause() {
- super.onPause()
- viewModel.submitAction(Action.StopListeningLocation)
- binding.mapView.onPause()
- }
-
- override fun onStop() {
- super.onStop()
- binding.mapView.onStop()
- }
-
- override fun onLowMemory() {
- super.onLowMemory()
- binding.mapView.onLowMemory()
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- binding.mapView.onDestroy()
- mapboxMap = null
- locationComponent = null
- inputJob = null
- _binding = null
- }
-}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt
deleted file mode 100644
index e71bfcc..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt
+++ /dev/null
@@ -1,53 +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.location
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.os.Bundle
-import android.util.AttributeSet
-import android.view.MotionEvent
-import com.mapbox.mapboxsdk.maps.MapView
-import com.mapbox.mapboxsdk.maps.OnMapReadyCallback
-
-class FakeLocationMapView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : MapView(context, attrs, defStyleAttr) {
-
- /**
- * Overrides onTouchEvent because this MapView is part of a scroll view
- * and we want this map view to consume all touch events originating on this view.
- */
- @SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(event: MotionEvent?): Boolean {
- when (event?.action) {
- MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true)
- MotionEvent.ACTION_UP -> parent.requestDisallowInterceptTouchEvent(false)
- }
- super.onTouchEvent(event)
- return true
- }
-}
-
-fun FakeLocationMapView.setup(savedInstanceState: Bundle?, callback: OnMapReadyCallback) =
- this.apply {
- onCreate(savedInstanceState)
- getMapAsync(callback)
- }
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt
deleted file mode 100644
index 50d7a14..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2022 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.location
-
-import android.location.Location
-import foundation.e.privacycentralapp.domain.entities.LocationMode
-
-data class FakeLocationState(
- val mode: LocationMode = LocationMode.REAL_LOCATION,
- val currentLocation: Location? = null,
- val specificLatitude: Float? = null,
- val specificLongitude: Float? = null,
- val forceRefresh: Boolean = false,
-)
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt
deleted file mode 100644
index 1cdf9f4..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt
+++ /dev/null
@@ -1,126 +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.location
-
-import android.location.Location
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.domain.entities.LocationMode
-import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase
-import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlin.time.Duration.Companion.milliseconds
-
-class FakeLocationViewModel(
- private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
- private val fakeLocationStateUseCase: FakeLocationStateUseCase
-) : ViewModel() {
- companion object {
- private val SET_SPECIFIC_LOCATION_DELAY = 200.milliseconds
- }
-
- private val _state = MutableStateFlow(FakeLocationState())
- val state = _state.asStateFlow()
-
- private val _singleEvents = MutableSharedFlow<SingleEvent>()
- val singleEvents = _singleEvents.asSharedFlow()
-
- private val specificLocationInputFlow = MutableSharedFlow<Action.SetSpecificLocationAction>()
-
- @OptIn(FlowPreview::class)
- suspend fun doOnStartedState() = withContext(Dispatchers.Main) {
- launch {
- merge(
- fakeLocationStateUseCase.configuredLocationMode.map { (mode, lat, lon) ->
- _state.update { s ->
- s.copy(
- mode = mode,
- specificLatitude = lat,
- specificLongitude = lon
- )
- }
- },
- specificLocationInputFlow
- .debounce(SET_SPECIFIC_LOCATION_DELAY).map { action ->
- fakeLocationStateUseCase.setSpecificLocation(action.latitude, action.longitude)
- }
- ).collect {}
- }
-
- launch {
- fakeLocationStateUseCase.currentLocation.collect { location ->
- _singleEvents.emit(
- SingleEvent.LocationUpdatedEvent(
- mode = _state.value.mode,
- location = location
- )
- )
- }
- }
- }
-
- fun submitAction(action: Action) = viewModelScope.launch {
- when (action) {
- is Action.StartListeningLocation -> actionStartListeningLocation()
- is Action.StopListeningLocation -> fakeLocationStateUseCase.stopListeningLocation()
- is Action.SetSpecificLocationAction -> setSpecificLocation(action)
- is Action.UseRandomLocationAction -> fakeLocationStateUseCase.setRandomLocation()
- is Action.UseRealLocationAction ->
- fakeLocationStateUseCase.stopFakeLocation()
- }
- }
-
- private suspend fun actionStartListeningLocation() {
- val started = fakeLocationStateUseCase.startListeningLocation()
- if (!started) {
- _singleEvents.emit(SingleEvent.RequestLocationPermission)
- }
- }
-
- private suspend fun setSpecificLocation(action: Action.SetSpecificLocationAction) {
- specificLocationInputFlow.emit(action)
- }
-
- sealed class SingleEvent {
- data class LocationUpdatedEvent(val mode: LocationMode, val location: Location?) : SingleEvent()
- object RequestLocationPermission : SingleEvent()
- data class ErrorEvent(val error: String) : SingleEvent()
- }
-
- sealed class Action {
- object StartListeningLocation : Action()
- object StopListeningLocation : Action()
- object UseRealLocationAction : Action()
- object UseRandomLocationAction : Action()
- data class SetSpecificLocationAction(
- val latitude: Float,
- val longitude: Float
- ) : Action()
- }
-}
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
deleted file mode 100644
index cb32c2c..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Copyright (C) 2021 E FOUNDATION, 2022 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.privacycentralapp.features.trackers
-
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.os.Bundle
-import android.text.Spannable
-import android.text.SpannableString
-import android.text.method.LinkMovementMethod
-import android.text.style.ClickableSpan
-import android.text.style.ForegroundColorSpan
-import android.text.style.UnderlineSpan
-import android.view.View
-import android.widget.Toast
-import androidx.core.content.ContextCompat
-import androidx.core.view.isVisible
-import androidx.fragment.app.commit
-import androidx.fragment.app.replace
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.LinearLayoutManager
-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.GraphHolder
-import foundation.e.privacycentralapp.common.NavToolbarFragment
-import foundation.e.privacycentralapp.common.setToolTipForAsterisk
-import foundation.e.privacycentralapp.databinding.FragmentTrackersBinding
-import foundation.e.privacycentralapp.databinding.TrackersItemGraphBinding
-import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics
-import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFragment
-import kotlinx.coroutines.launch
-
-class TrackersFragment :
- NavToolbarFragment(R.layout.fragment_trackers) {
-
- private val dependencyContainer: DependencyContainer by lazy {
- (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer
- }
-
- private val viewModel: TrackersViewModel by viewModels { dependencyContainer.viewModelsFactory }
-
- private var _binding: FragmentTrackersBinding? = null
- private val binding get() = _binding!!
-
- private var dayGraphHolder: GraphHolder? = null
- private var monthGraphHolder: GraphHolder? = null
- private var yearGraphHolder: GraphHolder? = null
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- _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)
- )
- }
- }
-
- val infoText = getString(R.string.trackers_info)
- val moreText = getString(R.string.trackers_info_more)
-
- val spannable = SpannableString("$infoText $moreText")
- val startIndex = infoText.length + 1
- val endIndex = spannable.length
- spannable.setSpan(
- ForegroundColorSpan(ContextCompat.getColor(requireContext(), R.color.accent)),
- startIndex,
- endIndex,
- Spannable.SPAN_INCLUSIVE_EXCLUSIVE
- )
- spannable.setSpan(UnderlineSpan(), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
- spannable.setSpan(
- object : ClickableSpan() {
- override fun onClick(p0: View) {
- viewModel.submitAction(TrackersViewModel.Action.ClickLearnMore)
- }
- },
- startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
- )
-
- with(binding.trackersInfo) {
- linksClickable = true
- isClickable = true
- 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)
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.singleEvents.collect { event ->
- when (event) {
- is TrackersViewModel.SingleEvent.ErrorEvent -> {
- displayToast(event.error)
- }
- is TrackersViewModel.SingleEvent.OpenAppDetailsEvent -> {
- requireActivity().supportFragmentManager.commit {
- replace<AppTrackersFragment>(
- R.id.container,
- args = AppTrackersFragment.buildArgs(
- event.appDesc.label.toString(),
- event.appDesc.packageName,
- event.appDesc.uid
- )
- )
- setReorderingAllowed(true)
- addToBackStack("apptrackers")
- }
- }
- 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()
- }
- }
- }
- }
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.doOnStartedState()
- }
- }
- }
-
- private fun displayToast(message: String) {
- Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
- .show()
- }
-
- override fun getTitle() = getString(R.string.trackers_title)
-
- 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) }
-
- state.apps?.let {
- binding.apps.post {
- (binding.apps.adapter as AppsAdapter?)?.dataSet = it
- }
- }
- }
-
- private fun renderGraph(
- statistics: TrackersPeriodicStatistics,
- graphHolder: GraphHolder,
- graphBinding: TrackersItemGraphBinding
- ) {
- if (statistics.callsBlockedNLeaked.all { it.first == 0 && it.second == 0 }) {
- graphBinding.graph.visibility = View.INVISIBLE
- graphBinding.graphEmpty.isVisible = true
- } else {
- graphBinding.graph.isVisible = true
- graphBinding.graphEmpty.isVisible = false
- graphHolder.data = statistics.callsBlockedNLeaked
- graphHolder.labels = statistics.periods
- graphBinding.trackersCountLabel.text =
- getString(R.string.trackers_count_label, statistics.trackersCount)
- }
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- dayGraphHolder = null
- monthGraphHolder = null
- yearGraphHolder = null
- _binding = null
- }
-}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt
deleted file mode 100644
index a3bb80a..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2022 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 foundation.e.privacycentralapp.domain.entities.AppWithCounts
-import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics
-
-data class TrackersState(
- val dayStatistics: TrackersPeriodicStatistics? = null,
- val monthStatistics: TrackersPeriodicStatistics? = null,
- val yearStatistics: TrackersPeriodicStatistics? = null,
- val apps: List<AppWithCounts>? = null,
-)
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
deleted file mode 100644
index 8b5cc32..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2021 E FOUNDATION, 2022 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.privacycentralapp.features.trackers
-
-import android.net.Uri
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.domain.entities.AppWithCounts
-import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase
-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
-) : 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()
-
- private val _singleEvents = MutableSharedFlow<SingleEvent>()
- val singleEvents = _singleEvents.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.getAppsWithCounts().map {
- _state.update { s -> s.copy(apps = it) }
- }
- ).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)))
- }
- }
-
- private suspend fun actionClickApp(action: Action.ClickAppAction) {
- state.value.apps?.find { it.uid == action.appUid }?.let {
- _singleEvents.emit(SingleEvent.OpenAppDetailsEvent(it))
- }
- }
-
- sealed class SingleEvent {
- data class ErrorEvent(val error: String) : SingleEvent()
- data class OpenAppDetailsEvent(val appDesc: AppWithCounts) : 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/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt
deleted file mode 100644
index 888c140..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright (C) 2023 MURENA SAS
- * 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.content.ActivityNotFoundException
-import android.content.Intent
-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.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.google.android.material.snackbar.Snackbar
-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 kotlinx.coroutines.launch
-
-class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) {
- companion object {
- private val PARAM_LABEL = "PARAM_LABEL"
- private val PARAM_PACKAGE_NAME = "PARAM_PACKAGE_NAME"
-
- const val PARAM_APP_UID = "PARAM_APP_UID"
-
- fun buildArgs(label: String, packageName: String, appUid: Int): Bundle = bundleOf(
- PARAM_LABEL to label,
- PARAM_PACKAGE_NAME to packageName,
- PARAM_APP_UID to appUid
- )
- }
-
- private val dependencyContainer: DependencyContainer by lazy {
- (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer
- }
-
- private val viewModel: AppTrackersViewModel by viewModels {
- dependencyContainer.viewModelsFactory
- }
-
- private var _binding: ApptrackersFragmentBinding? = null
- private val binding get() = _binding!!
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (arguments == null ||
- requireArguments().getInt(PARAM_APP_UID, Int.MIN_VALUE) == Int.MIN_VALUE
- ) {
- activity?.supportFragmentManager?.popBackStack()
- }
- }
-
- 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.setOnClickListener {
- viewModel.submitAction(AppTrackersViewModel.Action.BlockAllToggleAction(binding.blockAllToggle.isChecked))
- }
- binding.btnReset.setOnClickListener {
- viewModel.submitAction(AppTrackersViewModel.Action.ResetAllTrackers)
- }
-
- binding.trackers.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)) },
- )
- }
-
- 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()
- }
- }
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.doOnStartedState()
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- render(viewModel.state.value)
- viewModel.state.collect(::render)
- }
- }
- }
-
- private fun render(state: AppTrackersState) {
- binding.trackersCountSummary.text = if (state.getTrackersCount() == 0) ""
- else getString(
- R.string.apptrackers_trackers_count_summary,
- state.getBlockedTrackersCount(),
- state.getTrackersCount(),
- state.blocked,
- state.leaked
- )
-
- 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
- )
- }
- binding.noTrackersYet.isVisible = false
- binding.btnReset.isVisible = true
- } else {
- binding.trackersListTitle.isVisible = false
- binding.trackers.isVisible = false
- binding.noTrackersYet.isVisible = true
- binding.noTrackersYet.text = getString(
- when {
- !state.isBlockingActivated -> R.string.apptrackers_no_trackers_yet_block_off
- state.isWhitelistEmpty -> R.string.apptrackers_no_trackers_yet_block_on
- else -> R.string.app_trackers_no_trackers_yet_remaining_whitelist
- }
- )
- binding.btnReset.isVisible = state.isBlockingActivated && !state.isWhitelistEmpty
- }
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt
deleted file mode 100644
index a190a74..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2023 MURENA SAS
- * Copyright (C) 2022 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 foundation.e.privacymodules.permissions.data.ApplicationDescription
-import foundation.e.privacymodules.trackers.api.Tracker
-
-data class AppTrackersState(
- val appDesc: ApplicationDescription? = null,
- val isBlockingActivated: Boolean = false,
- val trackersWithWhiteList: List<Pair<Tracker, Boolean>>? = null,
- 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() = trackersWithWhiteList?.size ?: 0
- fun getBlockedTrackersCount(): Int = if (isTrackersBlockingEnabled && isBlockingActivated)
- trackersWithWhiteList?.count { !it.second } ?: 0
- else 0
-}
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
deleted file mode 100644
index e5a94f9..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright (C) 2023 MURENA SAS
- * 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.net.Uri
-import androidx.annotation.StringRes
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import foundation.e.privacycentralapp.domain.entities.TrackerMode
-import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase
-import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase
-import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase
-import foundation.e.privacymodules.permissions.data.ApplicationDescription
-import foundation.e.privacymodules.trackers.api.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 AppTrackersViewModel(
- private val app: ApplicationDescription,
- private val trackersStateUseCase: TrackersStateUseCase,
- private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
- private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase
-) : ViewModel() {
- companion object {
- private const val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/trackers/"
- }
-
- private val _state = MutableStateFlow(AppTrackersState())
- val state = _state.asStateFlow()
-
- private val _singleEvents = MutableSharedFlow<SingleEvent>()
- 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)
- )
- }
- }
- }
-
- 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 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()
- }
- }
-
- private suspend fun blockAllToggleAction(action: Action.BlockAllToggleAction) {
- withContext(Dispatchers.IO) {
- if (!state.value.isTrackersBlockingEnabled) {
- _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
- }
- trackersStateUseCase.toggleAppWhitelist(app, !action.isBlocked)
- _state.update {
- it.copy(
- isBlockingActivated = !trackersStateUseCase.isWhitelisted(app)
- )
- }
- }
- }
-
- private suspend fun toggleTrackerAction(action: Action.ToggleTrackerAction) {
- withContext(Dispatchers.IO) {
- if (!state.value.isTrackersBlockingEnabled) {
- _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
- }
-
- if (state.value.isBlockingActivated) {
- trackersStateUseCase.blockTracker(app, action.tracker, action.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) {
- }
- }
- }
- }
-
- private suspend fun resetAllTrackers() {
- withContext(Dispatchers.IO) {
- trackersStateUseCase.clearWhitelist(app)
- updateWhitelist()
- }
- }
- private fun fetchStatistics() {
- val (blocked, leaked) = trackersStatisticsUseCase.getCalls(app)
- return _state.update { s ->
- s.copy(
- trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app),
- leaked = leaked,
- blocked = blocked,
- isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
- )
- }
- }
-
- private fun updateWhitelist() {
- _state.update { s ->
- s.copy(
- trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app),
- isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
- )
- }
- }
-
- sealed class SingleEvent {
- data class ErrorEvent(@StringRes val errorResId: Int) : SingleEvent()
- 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/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt
deleted file mode 100644
index 197f13f..0000000
--- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt
+++ /dev/null
@@ -1,92 +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.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.privacycentralapp.R
-import foundation.e.privacymodules.trackers.api.Tracker
-
-class ToggleTrackersAdapter(
- private val itemsLayout: Int,
- private val onToggleSwitch: (Tracker, Boolean) -> Unit,
- private val onClickTitle: (Tracker) -> Unit
-) : 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)
-
- val toggle: Switch = view.findViewById(R.id.toggle)
-
- 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
- }
-
- toggle.isChecked = item.second
- toggle.isEnabled = isEnabled
-
- toggle.setOnClickListener {
- onToggleSwitch(item.first, toggle.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
- 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)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val permission = dataSet[position]
- holder.bind(permission, isEnabled)
- }
-
- override fun getItemCount(): Int = dataSet.size
-}