diff options
| author | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-05-02 21:25:17 +0200 |
|---|---|---|
| committer | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-05-02 22:00:35 +0200 |
| commit | a8874167f663885f2d3371801cf03681576ac817 (patch) | |
| tree | 5be07b8768142efeade536d4135f2250c1ac9071 /app/src/main/java/foundation/e/privacycentralapp/features | |
| parent | a0ee04ea9dbc0802c828afdf660eb37dc6fa350f (diff) | |
| download | advanced-privacy-a8874167f663885f2d3371801cf03681576ac817.tar.gz | |
1200: rename everything to AdvancedPrivacy
Diffstat (limited to 'app/src/main/java/foundation/e/privacycentralapp/features')
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 -} |
