diff options
author | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2022-04-26 18:20:18 +0000 |
---|---|---|
committer | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2022-04-26 18:20:18 +0000 |
commit | 1a7460b9e2e56697bd70b5c4da1321efdd09f19c (patch) | |
tree | b77ee228bfb4dad7c40f0e5b1f1b94e1ebc5df99 /app/src | |
parent | d0f8ec47a5780ba7de15e3d1ebcaa9a1df77da43 (diff) | |
parent | f9f49a9890dc82e0318ed52b7bafa1ce1da38cb5 (diff) |
Merge branch '5290_snackbar_qp_disabled' into 'main'
5290 : allow settings while main toggle off.
See merge request e/privacy-central/privacycentralapp!46
Diffstat (limited to 'app/src')
25 files changed, 331 insertions, 220 deletions
diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt index 4a790c6..727d00d 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt @@ -78,7 +78,7 @@ class DependencyContainer(val app: Application) { // Usecases val getQuickPrivacyStateUseCase by lazy { - GetQuickPrivacyStateUseCase(localStateRepository) + GetQuickPrivacyStateUseCase(localStateRepository, GlobalScope) } private val ipScramblingStateUseCase by lazy { IpScramblingStateUseCase( @@ -119,7 +119,7 @@ class DependencyContainer(val app: Application) { } val trackersViewModelFactory by lazy { - TrackersViewModelFactory(trackersStatisticsUseCase) + TrackersViewModelFactory(getQuickPrivacyStateUseCase, trackersStatisticsUseCase) } val appTrackersViewModelFactory by lazy { diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/QuickPrivacyDisabledSnackbar.kt b/app/src/main/java/foundation/e/privacycentralapp/common/QuickPrivacyDisabledSnackbar.kt new file mode 100644 index 0000000..705f65d --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/common/QuickPrivacyDisabledSnackbar.kt @@ -0,0 +1,36 @@ +/* + * 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.common + +import android.view.View +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.Snackbar +import foundation.e.privacycentralapp.R + +fun initQuickPrivacySnackbar(view: View, onDismiss: () -> Unit): Snackbar { + val snackbar = Snackbar.make(view, R.string.quickprivacy_disabled_message, Snackbar.LENGTH_INDEFINITE) + snackbar.setAction(R.string.close) { onDismiss() } + + snackbar.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (event == DISMISS_EVENT_SWIPE) onDismiss() + } + }) + return snackbar +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt index 136b20f..b4bca0b 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt @@ -22,6 +22,7 @@ import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode import foundation.e.privacycentralapp.domain.entities.LocationMode import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class LocalStateRepository(context: Context) { companion object { @@ -84,6 +85,13 @@ class LocalStateRepository(context: Context) { val internetPrivacyMode: MutableStateFlow<InternetPrivacyMode> = MutableStateFlow(InternetPrivacyMode.REAL_IP) + private val _showQuickPrivacyDisabledMessage = MutableStateFlow(false) + val showQuickPrivacyDisabledMessage: StateFlow<Boolean> = _showQuickPrivacyDisabledMessage + + fun setShowQuickPrivacyDisabledMessage(show: Boolean) { + _showQuickPrivacyDisabledMessage.value = show + } + var firstBoot: Boolean get() = sharedPref.getBoolean(KEY_FIRST_BOOT, true) set(value) = set(KEY_FIRST_BOOT, value) diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt index c07657a..fb773b2 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt @@ -33,6 +33,7 @@ import foundation.e.privacymodules.permissions.data.AppOpModes import foundation.e.privacymodules.permissions.data.ApplicationDescription import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlin.random.Random @@ -46,8 +47,8 @@ class FakeLocationStateUseCase( private val appContext: Context, private val coroutineScope: CoroutineScope ) { - // private val _locationMode = MutableStateFlow(LocationMode.REAL_LOCATION) - // val locationMode: StateFlow<LocationMode> = _locationMode + private val _configuredLocationMode = MutableStateFlow<Triple<LocationMode, Float?, Float?>>(Triple(LocationMode.REAL_LOCATION, null, null)) + val configuredLocationMode: StateFlow<Triple<LocationMode, Float?, Float?>> = _configuredLocationMode init { coroutineScope.launch { @@ -60,44 +61,23 @@ class FakeLocationStateUseCase( private val locationManager: LocationManager get() = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager - fun getLocationMode(): Triple<LocationMode, Float?, Float?> { - val fakeLocation = localStateRepository.fakeLocation - return if (fakeLocation != null && localStateRepository.locationMode.value == LocationMode.SPECIFIC_LOCATION) { - Triple( - LocationMode.SPECIFIC_LOCATION, - fakeLocation.first, - fakeLocation.second - ) - } else { - Triple(localStateRepository.locationMode.value, null, null) - } - } - private fun acquireLocationPermission() { permissionsModule.toggleDangerousPermission( appDesc, android.Manifest.permission.ACCESS_FINE_LOCATION, true ) - - // permissionsModule.setAppOpMode( - // appDesc, AppOpsManager.OPSTR_COARSE_LOCATION, - // AppOpModes.ALLOWED - // ) - // permissionsModule.setAppOpMode( - // appDesc, AppOpsManager.OPSTR_FINE_LOCATION, - // AppOpModes.ALLOWED - // ) } private fun applySettings(isQuickPrivacyEnabled: Boolean, fakeLocation: Pair<Float, Float>?) { + _configuredLocationMode.value = computeLocationMode(fakeLocation) + if (isQuickPrivacyEnabled && fakeLocation != null) { if (permissionsModule.getAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION) != AppOpModes.ALLOWED) { permissionsModule.setAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpModes.ALLOWED) } fakeLocationModule.startFakeLocation() fakeLocationModule.setFakeLocation(fakeLocation.first.toDouble(), fakeLocation.second.toDouble()) - localStateRepository.locationMode.value = if (fakeLocation in citiesRepository.citiesLocationsList) LocationMode.RANDOM_LOCATION - else LocationMode.SPECIFIC_LOCATION + localStateRepository.locationMode.value = configuredLocationMode.value.first } else { fakeLocationModule.stopFakeLocation() localStateRepository.locationMode.value = LocationMode.REAL_LOCATION @@ -105,10 +85,18 @@ class FakeLocationStateUseCase( } fun setSpecificLocation(latitude: Float, longitude: Float) { + if (!localStateRepository.isQuickPrivacyEnabled) { + localStateRepository.setShowQuickPrivacyDisabledMessage(true) + } + setFakeLocation(latitude to longitude) } fun setRandomLocation() { + if (!localStateRepository.isQuickPrivacyEnabled) { + localStateRepository.setShowQuickPrivacyDisabledMessage(true) + } + val randomIndex = Random.nextInt(citiesRepository.citiesLocationsList.size) val location = citiesRepository.citiesLocationsList[randomIndex] @@ -117,12 +105,27 @@ class FakeLocationStateUseCase( private fun setFakeLocation(location: Pair<Float, Float>) { localStateRepository.fakeLocation = location - applySettings(true, location) + applySettings(localStateRepository.isQuickPrivacyEnabled, location) } fun stopFakeLocation() { + if (!localStateRepository.isQuickPrivacyEnabled) { + localStateRepository.setShowQuickPrivacyDisabledMessage(true) + } + localStateRepository.fakeLocation = null - applySettings(true, null) + applySettings(localStateRepository.isQuickPrivacyEnabled, null) + } + + private fun computeLocationMode(fakeLocation: Pair<Float, Float>?): Triple<LocationMode, Float?, Float?> { + return Triple( + when { + fakeLocation == null -> LocationMode.REAL_LOCATION + fakeLocation in citiesRepository.citiesLocationsList -> LocationMode.RANDOM_LOCATION + else -> LocationMode.SPECIFIC_LOCATION + }, + fakeLocation?.first, fakeLocation?.second + ) } val currentLocation = MutableStateFlow<Location?>(null) diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt index fd9430c..36599cb 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt @@ -21,12 +21,26 @@ import foundation.e.privacycentralapp.data.repositories.LocalStateRepository import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode import foundation.e.privacycentralapp.domain.entities.LocationMode import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +class GetQuickPrivacyStateUseCase( + private val localStateRepository: LocalStateRepository, + private val coroutineScope: CoroutineScope +) { + + init { + coroutineScope.launch { + localStateRepository.quickPrivacyEnabledFlow.collect { + if (it) resetQuickPrivacyDisabledMessage() + } + } + } -class GetQuickPrivacyStateUseCase(private val localStateRepository: LocalStateRepository) { val quickPrivacyEnabledFlow = localStateRepository.quickPrivacyEnabledFlow - val isQuickPrivacyEnabled get() = localStateRepository.isQuickPrivacyEnabled val quickPrivacyState = combine( localStateRepository.quickPrivacyEnabledFlow, @@ -77,4 +91,10 @@ class GetQuickPrivacyStateUseCase(private val localStateRepository: LocalStateRe val newState = !localStateRepository.isQuickPrivacyEnabled return localStateRepository.setQuickPrivacyReturnIsFirstActivation(newState) } + + val showQuickPrivacyDisabledMessage: StateFlow<Boolean> = localStateRepository.showQuickPrivacyDisabledMessage + + fun resetQuickPrivacyDisabledMessage() { + localStateRepository.setShowQuickPrivacyDisabledMessage(false) + } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt index a701eec..0d25d16 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt @@ -25,6 +25,7 @@ import foundation.e.privacymodules.permissions.IPermissionsPrivacyModule import foundation.e.privacymodules.permissions.data.ApplicationDescription import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow @@ -41,6 +42,9 @@ class IpScramblingStateUseCase( coroutineScope: CoroutineScope ) { + val _configuredMode = MutableStateFlow(localStateRepository.isIpScramblingEnabled) + val configuredMode: StateFlow<Boolean> = _configuredMode + val internetPrivacyMode: StateFlow<InternetPrivacyMode> = callbackFlow { val listener = object : IIpScramblerModule.Listener { override fun onStatusChanged(newStatus: IIpScramblerModule.Status) { @@ -78,10 +82,14 @@ class IpScramblingStateUseCase( } fun toggle(hideIp: Boolean) { - if (!localStateRepository.isQuickPrivacyEnabled) return - localStateRepository.isIpScramblingEnabled = hideIp - applySettings(true, hideIp) + _configuredMode.value = hideIp + + if (!localStateRepository.isQuickPrivacyEnabled) { + localStateRepository.setShowQuickPrivacyDisabledMessage(true) + } else { + applySettings(true, hideIp) + } } private fun getHiddenPackageNames(): List<String> { diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt index 5263559..3319eb0 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt @@ -79,6 +79,10 @@ class TrackersStateUseCase( } fun toggleAppWhitelist(appUid: Int, isWhitelisted: Boolean) { + if (!localStateRepository.isQuickPrivacyEnabled) { + localStateRepository.setShowQuickPrivacyDisabledMessage(true) + } + if (appUid == appListsRepository.dummySystemApp.uid) { appListsRepository.getHiddenSystemApps().forEach { blockTrackersPrivacyModule.setWhiteListed(it.uid, isWhitelisted) @@ -89,6 +93,9 @@ class TrackersStateUseCase( } fun blockTracker(appUid: Int, tracker: Tracker, isBlocked: Boolean) { + if (!localStateRepository.isQuickPrivacyEnabled) { + localStateRepository.setShowQuickPrivacyDisabledMessage(true) + } if (appUid == appListsRepository.dummySystemApp.uid) { appListsRepository.getHiddenSystemApps().forEach { blockTrackersPrivacyModule.setWhiteListed(tracker, it.uid, !isBlocked) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt index 7270c32..3ed3168 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt @@ -57,7 +57,8 @@ class DashboardFeature( val trackersCount: Int? = null, val allowedTrackersCount: Int? = null, val dayStatistics: List<Pair<Int, Int>>? = null, - val dayLabels: List<String>? = null + val dayLabels: List<String>? = null, + val showQuickPrivacyDisabledMessage: Boolean = false ) sealed class SingleEvent { @@ -77,6 +78,7 @@ class DashboardFeature( object ShowAppsPermissions : Action() object ShowTrackers : Action() object FetchStatistics : Action() + object CloseQuickPrivacyDisabledMessage : Action() } sealed class Effect { @@ -99,6 +101,7 @@ class DashboardFeature( object NewStatisticsAvailablesEffect : Effect() object FirstIPTrackerActivationEffect : Effect() data class LocationHiddenUpdatedEffect(val isLocationHidden: Boolean) : Effect() + data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect() } companion object { @@ -129,7 +132,7 @@ class DashboardFeature( isLocationHidden = effect.isLocationHidden ) is Effect.UpdateLocationModeEffect -> state.copy(locationMode = effect.mode) - + is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show) else -> state } }, @@ -161,7 +164,10 @@ class DashboardFeature( }, getPrivacyStateUseCase.locationMode.map { Effect.UpdateLocationModeEffect(it) - } + }, + getPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map { + Effect.ShowQuickPrivacyDisabledMessageEffect(it) + }, ) Action.ShowFakeMyLocationAction -> flowOf(Effect.OpenFakeMyLocationEffect) Action.ShowAppsPermissions -> flowOf(Effect.OpenAppsPermissionsEffect) @@ -183,6 +189,10 @@ class DashboardFeature( ) } } + is Action.CloseQuickPrivacyDisabledMessage -> { + getPrivacyStateUseCase.resetQuickPrivacyDisabledMessage() + flowOf(Effect.NoEffect) + } } }, singleEventProducer = { _, _, effect -> diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt index ea470a2..32549c9 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt @@ -26,15 +26,17 @@ import android.widget.Toast import androidx.core.content.ContextCompat.getColor import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels -import androidx.fragment.app.add import androidx.fragment.app.commit +import androidx.fragment.app.replace import androidx.lifecycle.lifecycleScope +import com.google.android.material.snackbar.Snackbar import foundation.e.flowmvi.MVIView import foundation.e.privacycentralapp.DependencyContainer import foundation.e.privacycentralapp.PrivacyCentralApplication import foundation.e.privacycentralapp.R import foundation.e.privacycentralapp.common.GraphHolder import foundation.e.privacycentralapp.common.NavToolbarFragment +import foundation.e.privacycentralapp.common.initQuickPrivacySnackbar import foundation.e.privacycentralapp.databinding.FragmentDashboardBinding import foundation.e.privacycentralapp.domain.entities.LocationMode import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState @@ -65,6 +67,8 @@ class DashboardFragment : private lateinit var graphHolder: GraphHolder private lateinit var binding: FragmentDashboardBinding + private var qpDisabledSnackbar: Snackbar? = null + private var updateUIJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -79,14 +83,14 @@ class DashboardFragment : when (event) { is DashboardFeature.SingleEvent.NavigateToLocationSingleEvent -> { requireActivity().supportFragmentManager.commit { - add<FakeLocationFragment>(R.id.container) + replace<FakeLocationFragment>(R.id.container) setReorderingAllowed(true) addToBackStack("dashboard") } } is DashboardFeature.SingleEvent.NavigateToInternetActivityPrivacySingleEvent -> { requireActivity().supportFragmentManager.commit { - add<InternetPrivacyFragment>(R.id.container) + replace<InternetPrivacyFragment>(R.id.container) setReorderingAllowed(true) addToBackStack("dashboard") } @@ -97,7 +101,7 @@ class DashboardFragment : } DashboardFeature.SingleEvent.NavigateToTrackersSingleEvent -> { requireActivity().supportFragmentManager.commit { - add<TrackersFragment>(R.id.container) + replace<TrackersFragment>(R.id.container) setReorderingAllowed(true) addToBackStack("dashboard") } @@ -138,6 +142,10 @@ class DashboardFragment : binding.amITracked.container.setOnClickListener { viewModel.submitAction(DashboardFeature.Action.ShowTrackers) } + + qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) { + viewModel.submitAction(DashboardFeature.Action.CloseQuickPrivacyDisabledMessage) + } } override fun onResume() { @@ -149,6 +157,8 @@ class DashboardFragment : } } + render(viewModel.dashboardFeature.state.value) + viewModel.submitAction(DashboardFeature.Action.FetchStatistics) } @@ -162,6 +172,9 @@ class DashboardFragment : } override fun render(state: State) { + if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show() + else qpDisabledSnackbar?.dismiss() + binding.stateLabel.text = getString( when (state.quickPrivacyState) { QuickPrivacyState.DISABLED -> R.string.dashboard_state_title_off diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt index eca1578..8e4318d 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -63,7 +64,8 @@ class InternetPrivacyFeature( val bypassTorApps: Collection<String>, val selectedLocation: String, val availableLocationIds: List<String>, - val forceRedraw: Boolean = false + val forceRedraw: Boolean = false, + val showQuickPrivacyDisabledMessage: Boolean = false ) { fun getApps(): List<Pair<ApplicationDescription, Boolean>> { return availableApps.map { it to (it.packageName !in bypassTorApps) } @@ -84,6 +86,7 @@ class InternetPrivacyFeature( data class AndroidVpnActivityResultAction(val resultCode: Int) : Action() data class ToggleAppIpScrambled(val packageName: String) : Action() data class SelectLocationAction(val position: Int) : Action() + object CloseQuickPrivacyDisabledMessage : Action() } sealed class Effect { @@ -100,6 +103,7 @@ class InternetPrivacyFeature( data class LocationSelectedEffect(val locationId: String) : Effect() object WarningStartingLongEffect : Effect() data class ErrorEffect(val message: String) : Effect() + data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect() } companion object { @@ -131,6 +135,7 @@ class InternetPrivacyFeature( ) is Effect.LocationSelectedEffect -> state.copy(selectedLocation = effect.locationId) Effect.QuickPrivacyDisabledWarningEffect -> state.copy(forceRedraw = !state.forceRedraw) + is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show) else -> state } }, @@ -139,9 +144,24 @@ class InternetPrivacyFeature( action is Action.LoadInternetModeAction -> merge( getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow .map { Effect.QuickPrivacyUpdatedEffect(it) }, - ipScramblingStateUseCase.internetPrivacyMode - .map { Effect.ModeUpdatedEffect(it) } - .shareIn(scope = coroutineScope, started = SharingStarted.Lazily, replay = 0), + getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map { + Effect.ShowQuickPrivacyDisabledMessageEffect(it) + }, + getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow.flatMapLatest { enabled -> + if (enabled) ipScramblingStateUseCase.internetPrivacyMode + .map { Effect.ModeUpdatedEffect(it) } + .shareIn( + scope = coroutineScope, + started = SharingStarted.Lazily, + replay = 0 + ) + else ipScramblingStateUseCase.configuredMode.map { + Effect.ModeUpdatedEffect( + if (it) InternetPrivacyMode.HIDE_IP + else InternetPrivacyMode.REAL_IP + ) + } + }, appListUseCase.getAppsUsingInternet().map { apps -> Effect.AvailableAppsListEffect( apps, @@ -175,24 +195,16 @@ class InternetPrivacyFeature( InternetPrivacyMode.HIDE_IP_LOADING, InternetPrivacyMode.REAL_IP_LOADING ) -> { - if (getQuickPrivacyStateUseCase.isQuickPrivacyEnabled) { - ipScramblingStateUseCase.toggle(hideIp = false) - flowOf(Effect.ModeUpdatedEffect(InternetPrivacyMode.REAL_IP_LOADING)) - } else { - flowOf(Effect.QuickPrivacyDisabledWarningEffect) - } + ipScramblingStateUseCase.toggle(hideIp = false) + flowOf(Effect.ModeUpdatedEffect(InternetPrivacyMode.REAL_IP_LOADING)) } action is Action.UseHiddenIPAction && state.mode in listOf( InternetPrivacyMode.REAL_IP, InternetPrivacyMode.REAL_IP_LOADING ) -> { - if (getQuickPrivacyStateUseCase.isQuickPrivacyEnabled) { - ipScramblingStateUseCase.toggle(hideIp = true) - flowOf(Effect.ModeUpdatedEffect(InternetPrivacyMode.HIDE_IP_LOADING)) - } else { - flowOf(Effect.QuickPrivacyDisabledWarningEffect) - } + ipScramblingStateUseCase.toggle(hideIp = true) + flowOf(Effect.ModeUpdatedEffect(InternetPrivacyMode.HIDE_IP_LOADING)) } action is Action.ToggleAppIpScrambled -> { @@ -208,6 +220,10 @@ class InternetPrivacyFeature( flowOf(Effect.NoEffect) } } + action is Action.CloseQuickPrivacyDisabledMessage -> { + getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage() + flowOf(Effect.NoEffect) + } else -> flowOf(Effect.NoEffect) } }, @@ -216,8 +232,6 @@ class InternetPrivacyFeature( effect is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) effect is Effect.WarningStartingLongEffect -> SingleEvent.ErrorEvent(R.string.ipscrambling_warning_starting_long) - effect is Effect.QuickPrivacyDisabledWarningEffect -> SingleEvent.ErrorEvent(error = R.string.ipscrambling_error_quickprivacy_disabled) - action is Action.UseHiddenIPAction && effect is Effect.ShowAndroidVpnDisclaimerEffect -> SingleEvent.StartAndroidVpnActivityEvent(effect.intent) 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 index f49399f..2452d33 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt @@ -26,12 +26,14 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar import foundation.e.flowmvi.MVIView import foundation.e.privacycentralapp.DependencyContainer import foundation.e.privacycentralapp.PrivacyCentralApplication import foundation.e.privacycentralapp.R import foundation.e.privacycentralapp.common.NavToolbarFragment import foundation.e.privacycentralapp.common.ToggleAppsAdapter +import foundation.e.privacycentralapp.common.initQuickPrivacySnackbar import foundation.e.privacycentralapp.databinding.FragmentInternetActivityPolicyBinding import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode import foundation.e.privacycentralapp.extensions.toText @@ -56,6 +58,8 @@ class InternetPrivacyFragment : private lateinit var binding: FragmentInternetActivityPolicyBinding + private var qpDisabledSnackbar: Snackbar? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launchWhenStarted { @@ -132,12 +136,19 @@ class InternetPrivacyFragment : } } + qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) { + viewModel.submitAction(InternetPrivacyFeature.Action.CloseQuickPrivacyDisabledMessage) + } + binding.executePendingBindings() } override fun getTitle(): String = getString(R.string.ipscrambling_title) override fun render(state: InternetPrivacyFeature.State) { + if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show() + else qpDisabledSnackbar?.dismiss() + binding.radioUseHiddenIp.radiobutton.apply { isChecked = state.mode in listOf( InternetPrivacyMode.HIDE_IP, diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt index a7869ce..85a507d 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt @@ -27,7 +27,6 @@ import foundation.e.privacycentralapp.domain.entities.LocationMode import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -48,16 +47,16 @@ class FakeLocationFeature( singleEventProducer ) { data class State( - val isEnabled: Boolean, - val mode: LocationMode, - val currentLocation: Location?, + val mode: LocationMode = LocationMode.REAL_LOCATION, + val currentLocation: Location? = null, val specificLatitude: Float? = null, val specificLongitude: Float? = null, - val forceRefresh: Boolean = false + val forceRefresh: Boolean = false, + val showQuickPrivacyDisabledMessage: Boolean = false ) sealed class SingleEvent { - data class LocationUpdatedEvent(val location: Location?) : SingleEvent() + data class LocationUpdatedEvent(val mode: LocationMode, val location: Location?) : SingleEvent() data class ErrorEvent(val error: String) : SingleEvent() } @@ -70,10 +69,10 @@ class FakeLocationFeature( val latitude: Float, val longitude: Float ) : Action() + object CloseQuickPrivacyDisabledMessage : Action() } sealed class Effect { - data class QuickPrivacyUpdatedEffect(val isEnabled: Boolean) : Effect() data class LocationModeUpdatedEffect( val mode: LocationMode, val latitude: Float? = null, @@ -81,17 +80,13 @@ class FakeLocationFeature( ) : Effect() data class LocationUpdatedEffect(val location: Location?) : Effect() data class ErrorEffect(val message: String) : Effect() - object QuickPrivacyDisabledWarningEffect : Effect() object NoEffect : Effect() + data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect() } companion object { fun create( - initialState: State = State( - isEnabled = false, - mode = LocationMode.REAL_LOCATION, - currentLocation = null - ), + initialState: State = State(), getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, fakeLocationStateUseCase: FakeLocationStateUseCase, coroutineScope: CoroutineScope @@ -99,66 +94,56 @@ class FakeLocationFeature( initialState, coroutineScope, reducer = { state, effect -> when (effect) { - is Effect.QuickPrivacyUpdatedEffect -> state.copy(isEnabled = effect.isEnabled) is Effect.LocationModeUpdatedEffect -> state.copy( mode = effect.mode, specificLatitude = effect.latitude, specificLongitude = effect.longitude ) - Effect.QuickPrivacyDisabledWarningEffect -> state.copy(forceRefresh = !state.forceRefresh) + is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show) else -> state } }, - actor = { state, action -> + actor = { _, action -> when (action) { - is Action.Init -> merge( - getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow.map { Effect.QuickPrivacyUpdatedEffect(it) }, - flow { - fakeLocationStateUseCase.startListeningLocation() - val (mode, lat, lon) = fakeLocationStateUseCase.getLocationMode() - emit(Effect.LocationModeUpdatedEffect(mode = mode, latitude = lat, longitude = lon)) - }, - fakeLocationStateUseCase.currentLocation.map { Effect.LocationUpdatedEffect(it) } - ) + is Action.Init -> { + fakeLocationStateUseCase.startListeningLocation() + merge( + fakeLocationStateUseCase.configuredLocationMode.map { (mode, lat, lon) -> + Effect.LocationModeUpdatedEffect(mode = mode, latitude = lat, longitude = lon) + }, + fakeLocationStateUseCase.currentLocation.map { Effect.LocationUpdatedEffect(it) }, + getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map { Effect.ShowQuickPrivacyDisabledMessageEffect(it) }, + ) + } is Action.LeaveScreen -> { fakeLocationStateUseCase.stopListeningLocation() flowOf(Effect.NoEffect) } is Action.SetSpecificLocationAction -> { - if (state.isEnabled) { - fakeLocationStateUseCase.setSpecificLocation( - action.latitude, - action.longitude - ) - flowOf( - Effect.LocationModeUpdatedEffect( - mode = LocationMode.SPECIFIC_LOCATION, - latitude = action.latitude, - longitude = action.longitude - ) - ) - } else flowOf(Effect.QuickPrivacyDisabledWarningEffect) + fakeLocationStateUseCase.setSpecificLocation( + action.latitude, + action.longitude + ) + flowOf(Effect.NoEffect) } is Action.UseRandomLocationAction -> { - if (state.isEnabled) { - fakeLocationStateUseCase.setRandomLocation() - flowOf(Effect.LocationModeUpdatedEffect(LocationMode.RANDOM_LOCATION)) - } else flowOf(Effect.QuickPrivacyDisabledWarningEffect) + fakeLocationStateUseCase.setRandomLocation() + flowOf(Effect.NoEffect) } is Action.UseRealLocationAction -> { - if (state.isEnabled) { - fakeLocationStateUseCase.stopFakeLocation() - flowOf(Effect.LocationModeUpdatedEffect(LocationMode.REAL_LOCATION)) - } else flowOf(Effect.QuickPrivacyDisabledWarningEffect) + fakeLocationStateUseCase.stopFakeLocation() + flowOf(Effect.NoEffect) + } + is Action.CloseQuickPrivacyDisabledMessage -> { + getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage() + flowOf(Effect.NoEffect) } } }, - singleEventProducer = { _, _, effect -> + singleEventProducer = { state, _, effect -> when (effect) { is Effect.LocationUpdatedEffect -> - SingleEvent.LocationUpdatedEvent(effect.location) - Effect.QuickPrivacyDisabledWarningEffect -> - SingleEvent.ErrorEvent("Enabled Quick Privacy to use functionalities") + SingleEvent.LocationUpdatedEvent(state.mode, effect.location) is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) else -> null } 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 index bc35521..fa26dd0 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt @@ -29,6 +29,7 @@ import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM @@ -48,6 +49,7 @@ 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.initQuickPrivacySnackbar import foundation.e.privacycentralapp.databinding.FragmentFakeLocationBinding import foundation.e.privacycentralapp.domain.entities.LocationMode import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf @@ -78,6 +80,8 @@ class FakeLocationFragment : private var mapboxMap: MapboxMap? = null private var locationComponent: LocationComponent? = null + private var qpDisabledSnackbar: Snackbar? = null + private var inputJob: Job? = null companion object { @@ -97,18 +101,7 @@ class FakeLocationFragment : displayToast(event.error) } is FakeLocationFeature.SingleEvent.LocationUpdatedEvent -> { - if (isFirstLaunch && mapboxMap != null) { - mapboxMap?.moveCamera( - CameraUpdateFactory.newLatLng( - LatLng( - event.location?.latitude ?: 0.0, - event.location?.longitude ?: 0.0 - ) - ) - ) - isFirstLaunch = false - } - updateLocation(event.location) + updateLocation(event.location, event.mode) } } } @@ -151,8 +144,14 @@ class FakeLocationFragment : } // Bind click listeners once map is ready. bindClickListeners() + + render(viewModel.fakeLocationFeature.state.value) } } + + qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) { + viewModel.submitAction(Action.CloseQuickPrivacyDisabledMessage) + } } private fun getCoordinatesAfterTextChanged( @@ -232,28 +231,22 @@ class FakeLocationFragment : @SuppressLint("MissingPermission") override fun render(state: FakeLocationFeature.State) { - binding.radioUseRandomLocation.apply { - isChecked = state.mode == LocationMode.RANDOM_LOCATION - isEnabled = state.isEnabled - } + if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show() + else qpDisabledSnackbar?.dismiss() - binding.radioUseSpecificLocation.apply { - isChecked = state.mode == LocationMode.SPECIFIC_LOCATION - isEnabled = state.isEnabled - } + binding.radioUseRandomLocation.isChecked = state.mode == LocationMode.RANDOM_LOCATION - binding.radioUseRealLocation.apply { - isChecked = state.mode == LocationMode.REAL_LOCATION - isEnabled = state.isEnabled - } + 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.SPECIFIC_LOCATION) { + if (state.mode == LocationMode.REAL_LOCATION) { binding.centeredMarker.isVisible = false } else { binding.mapLoader.isVisible = false - binding.mapOverlay.isVisible = false + binding.mapOverlay.isVisible = state.mode != LocationMode.SPECIFIC_LOCATION binding.centeredMarker.isVisible = true mapboxMap?.moveCamera( @@ -276,7 +269,7 @@ class FakeLocationFragment : override fun actions(): Flow<Action> = viewModel.actions @SuppressLint("MissingPermission") - private fun updateLocation(lastLocation: Location?) { + private fun updateLocation(lastLocation: Location?, mode: LocationMode) { lastLocation?.let { location -> locationComponent?.isLocationComponentEnabled = true val locationUpdate = LocationUpdate.Builder() @@ -285,18 +278,24 @@ class FakeLocationFragment : .build() locationComponent?.forceLocationUpdate(locationUpdate) - if (!binding.mapView.isEnabled) { + if (mode == LocationMode.REAL_LOCATION) { binding.mapLoader.isVisible = false binding.mapOverlay.isVisible = false - mapboxMap?.animateCamera( - CameraUpdateFactory.newLatLng( - LatLng(location.latitude, location.longitude) - ) + + 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 (!binding.mapView.isEnabled) { + if (mode == LocationMode.REAL_LOCATION) { binding.mapLoader.isVisible = true binding.mapOverlay.isVisible = true } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt index a606e49..34ddfbe 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt @@ -24,6 +24,7 @@ import foundation.e.flowmvi.SingleEventProducer import foundation.e.flowmvi.feature.BaseFeature import foundation.e.privacycentralapp.domain.entities.AppWithCounts import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics +import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flow @@ -51,6 +52,7 @@ class TrackersFeature( val monthStatistics: TrackersPeriodicStatistics? = null, val yearStatistics: TrackersPeriodicStatistics? = null, val apps: List<AppWithCounts>? = null, + val showQuickPrivacyDisabledMessage: Boolean = false ) sealed class SingleEvent { @@ -63,9 +65,11 @@ class TrackersFeature( object InitAction : Action() data class ClickAppAction(val packageName: String) : Action() object FetchStatistics : Action() + object CloseQuickPrivacyDisabledMessage : Action() } sealed class Effect { + object NoEffect : Effect() data class TrackersStatisticsLoadedEffect( val dayStatistics: TrackersPeriodicStatistics? = null, val monthStatistics: TrackersPeriodicStatistics? = null, @@ -75,14 +79,15 @@ class TrackersFeature( val apps: List<AppWithCounts> ) : Effect() data class OpenAppDetailsEffect(val appDesc: AppWithCounts) : Effect() - object QuickPrivacyDisabledWarningEffect : Effect() data class ErrorEffect(val message: String) : Effect() object NewStatisticsAvailablesEffect : Effect() + data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect() } companion object { fun create( initialState: State = State(), + getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, coroutineScope: CoroutineScope, trackersStatisticsUseCase: TrackersStatisticsUseCase ) = TrackersFeature( @@ -97,6 +102,7 @@ class TrackersFeature( is Effect.AvailableAppsListEffect -> state.copy(apps = effect.apps) is Effect.ErrorEffect -> state + is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show) else -> state } }, @@ -106,7 +112,10 @@ class TrackersFeature( flowOf(Effect.NewStatisticsAvailablesEffect), trackersStatisticsUseCase.listenUpdates().map { Effect.NewStatisticsAvailablesEffect - } + }, + getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map { + Effect.ShowQuickPrivacyDisabledMessageEffect(it) + }, ) is Action.ClickAppAction -> flowOf( @@ -131,13 +140,16 @@ class TrackersFeature( Effect.AvailableAppsListEffect(it) } ) + is Action.CloseQuickPrivacyDisabledMessage -> { + getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage() + flowOf(Effect.NoEffect) + } } }, singleEventProducer = { _, _, effect -> when (effect) { is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) is Effect.OpenAppDetailsEffect -> SingleEvent.OpenAppDetailsEvent(effect.appDesc) - is Effect.QuickPrivacyDisabledWarningEffect -> SingleEvent.ErrorEvent("Enabled Quick Privacy to use functionalities") is Effect.NewStatisticsAvailablesEffect -> SingleEvent.NewStatisticsAvailableSingleEvent else -> null } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt index 893f4ba..21a90bc 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt @@ -26,6 +26,7 @@ import androidx.fragment.app.replace import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar import foundation.e.flowmvi.MVIView import foundation.e.privacycentralapp.DependencyContainer import foundation.e.privacycentralapp.PrivacyCentralApplication @@ -33,6 +34,7 @@ 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.initQuickPrivacySnackbar import foundation.e.privacycentralapp.databinding.FragmentTrackersBinding import foundation.e.privacycentralapp.databinding.TrackersItemGraphBinding import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics @@ -57,6 +59,7 @@ class TrackersFragment : private lateinit var dayGraphHolder: GraphHolder private lateinit var monthGraphHolder: GraphHolder private lateinit var yearGraphHolder: GraphHolder + private var qpDisabledSnackbar: Snackbar? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -111,6 +114,10 @@ class TrackersFragment : ) } } + + qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) { + viewModel.submitAction(TrackersFeature.Action.CloseQuickPrivacyDisabledMessage) + } } override fun onResume() { @@ -121,6 +128,9 @@ class TrackersFragment : override fun getTitle() = getString(R.string.trackers_title) override fun render(state: TrackersFeature.State) { + if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show() + else qpDisabledSnackbar?.dismiss() + state.dayStatistics?.let { renderGraph(it, dayGraphHolder, binding.graphDay) } state.monthStatistics?.let { renderGraph(it, monthGraphHolder, binding.graphMonth) } state.yearStatistics?.let { renderGraph(it, yearGraphHolder, binding.graphYear) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt index c2a1822..4140381 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt @@ -20,12 +20,14 @@ package foundation.e.privacycentralapp.features.trackers import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import foundation.e.privacycentralapp.common.Factory +import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch class TrackersViewModel( + private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, private val trackersStatisticsUseCase: TrackersStatisticsUseCase ) : ViewModel() { @@ -35,7 +37,7 @@ class TrackersViewModel( val trackersFeature: TrackersFeature by lazy { TrackersFeature.create( coroutineScope = viewModelScope, - + getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase, trackersStatisticsUseCase = trackersStatisticsUseCase ) } @@ -48,10 +50,14 @@ class TrackersViewModel( } class TrackersViewModelFactory( + private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, private val trackersStatisticsUseCase: TrackersStatisticsUseCase ) : Factory<TrackersViewModel> { override fun create(): TrackersViewModel { - return TrackersViewModel(trackersStatisticsUseCase) + return TrackersViewModel( + getQuickPrivacyStateUseCase, + trackersStatisticsUseCase + ) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt index c1eef47..ad82337 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt @@ -55,7 +55,8 @@ class AppTrackersFeature( val isBlockingActivated: Boolean = false, val trackers: List<Tracker>? = null, val whitelist: List<String>? = null, - val isQuickPrivacyEnabled: Boolean = false + val isQuickPrivacyEnabled: Boolean = false, + val showQuickPrivacyDisabledMessage: Boolean = false ) { fun getTrackersStatus(): List<Pair<Tracker, Boolean>>? { if (trackers != null && whitelist != null) { @@ -83,6 +84,7 @@ class AppTrackersFeature( data class ToggleTrackerAction(val tracker: Tracker, val isBlocked: Boolean) : Action() data class ClickTracker(val tracker: Tracker) : Action() object FetchStatistics : Action() + object CloseQuickPrivacyDisabledMessage : Action() } sealed class Effect { @@ -94,8 +96,8 @@ class AppTrackersFeature( data class TrackersWhitelistUpdateEffect(val whitelist: List<String>) : Effect() object NewStatisticsAvailablesEffect : Effect() data class QuickPrivacyUpdatedEffect(val enabled: Boolean) : Effect() - object QuickPrivacyDisabledWarningEffect : Effect() data class OpenUrlEffect(val url: Uri) : Effect() + data class ShowQuickPrivacyDisabledMessageEffect(val show: Boolean) : Effect() } companion object { @@ -121,6 +123,7 @@ class AppTrackersFeature( state.copy(whitelist = effect.whitelist) is Effect.QuickPrivacyUpdatedEffect -> state.copy(isQuickPrivacyEnabled = effect.enabled) + is Effect.ShowQuickPrivacyDisabledMessageEffect -> state.copy(showQuickPrivacyDisabledMessage = effect.show) is Effect.ErrorEffect -> state else -> state } @@ -155,14 +158,15 @@ class AppTrackersFeature( }, getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow.map { Effect.QuickPrivacyUpdatedEffect(it) - } + }, + getQuickPrivacyStateUseCase.showQuickPrivacyDisabledMessage.map { + Effect.ShowQuickPrivacyDisabledMessageEffect(it) + }, ) } ?: flowOf(Effect.ErrorEffect(R.string.apptrackers_error_no_app)) is Action.BlockAllToggleAction -> - if (!state.isQuickPrivacyEnabled) { - flowOf(Effect.QuickPrivacyDisabledWarningEffect) - } else state.appDesc?.uid?.let { appUid -> + state.appDesc?.uid?.let { appUid -> flow { trackersStateUseCase.toggleAppWhitelist(appUid, !action.isBlocked) @@ -174,9 +178,7 @@ class AppTrackersFeature( } } ?: run { flowOf(Effect.ErrorEffect("No appDesc.")) } is Action.ToggleTrackerAction -> { - if (!state.isQuickPrivacyEnabled) { - flowOf(Effect.QuickPrivacyDisabledWarningEffect) - } else if (state.isBlockingActivated) { + if (state.isBlockingActivated) { state.appDesc?.uid?.let { appUid -> flow { trackersStateUseCase.blockTracker( @@ -210,15 +212,16 @@ class AppTrackersFeature( trackers = trackersStatisticsUseCase.getTrackers(it) ) } ?: Effect.ErrorEffect("No appDesc.") - ) + is Action.CloseQuickPrivacyDisabledMessage -> { + getQuickPrivacyStateUseCase.resetQuickPrivacyDisabledMessage() + flowOf(Effect.NoEffect) + } } }, singleEventProducer = { _, _, effect -> when (effect) { is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) - is Effect.QuickPrivacyDisabledWarningEffect -> - SingleEvent.ErrorEvent(R.string.apptrackers_error_quickprivacy_disabled) is Effect.NewStatisticsAvailablesEffect -> SingleEvent.NewStatisticsAvailableSingleEvent is Effect.OpenUrlEffect -> 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 index d6edee6..7e606af 100644 --- 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 @@ -27,11 +27,13 @@ import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar import foundation.e.flowmvi.MVIView import foundation.e.privacycentralapp.DependencyContainer import foundation.e.privacycentralapp.PrivacyCentralApplication import foundation.e.privacycentralapp.R import foundation.e.privacycentralapp.common.NavToolbarFragment +import foundation.e.privacycentralapp.common.initQuickPrivacySnackbar import foundation.e.privacycentralapp.databinding.ApptrackersFragmentBinding import foundation.e.privacycentralapp.extensions.toText import foundation.e.privacycentralapp.extensions.viewModelProviderFactoryOf @@ -65,6 +67,8 @@ class AppTrackersFragment : private lateinit var binding: ApptrackersFragmentBinding + private var qpDisabledSnackbar: Snackbar? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launchWhenStarted { @@ -109,10 +113,6 @@ class AppTrackersFragment : viewModel.submitAction(Action.BlockAllToggleAction(binding.blockAllToggle.isChecked)) } - binding.blockAllToggleClicker.setOnClickListener { - viewModel.submitAction(Action.BlockAllToggleAction(false)) - } - binding.trackers.apply { layoutManager = LinearLayoutManager(requireContext()) setHasFixedSize(true) @@ -124,6 +124,10 @@ class AppTrackersFragment : onClickTitle = { viewModel.submitAction(Action.ClickTracker(it)) } ) } + + qpDisabledSnackbar = initQuickPrivacySnackbar(binding.root) { + viewModel.submitAction(Action.CloseQuickPrivacyDisabledMessage) + } } override fun onResume() { @@ -132,6 +136,9 @@ class AppTrackersFragment : } override fun render(state: State) { + if (state.showQuickPrivacyDisabledMessage) qpDisabledSnackbar?.show() + else qpDisabledSnackbar?.dismiss() + binding.trackersCountSummary.text = if (state.getTrackersCount() == 0) "" else getString( R.string.apptrackers_trackers_count_summary, @@ -140,8 +147,6 @@ class AppTrackersFragment : ) binding.blockAllToggle.isChecked = state.isBlockingActivated - binding.blockAllToggle.isEnabled = state.isQuickPrivacyEnabled - binding.blockAllToggleClicker.isVisible = !state.isQuickPrivacyEnabled binding.trackersListTitle.isVisible = state.isBlockingActivated @@ -151,7 +156,7 @@ class AppTrackersFragment : binding.trackers.post { (binding.trackers.adapter as ToggleTrackersAdapter?)?.updateDataSet( trackersStatus, - state.isBlockingActivated && state.isQuickPrivacyEnabled + state.isBlockingActivated ) } binding.noTrackersYet.isVisible = false 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 index b9beccf..02a274a 100644 --- 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 @@ -25,7 +25,6 @@ import android.view.ViewGroup import android.widget.Switch import android.widget.TextView import androidx.core.content.ContextCompat -import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import foundation.e.privacycentralapp.R import foundation.e.privacymodules.trackers.Tracker @@ -46,7 +45,6 @@ class ToggleTrackersAdapter( val title: TextView = view.findViewById(R.id.title) val toggle: Switch = view.findViewById(R.id.toggle) - val toggleOverlay: View = view.findViewById(R.id.toggle_clicker) fun bind(item: Pair<Tracker, Boolean>, isEnabled: Boolean) { val text = item.first.label @@ -62,14 +60,10 @@ class ToggleTrackersAdapter( toggle.isChecked = item.second toggle.isEnabled = isEnabled - toggleOverlay.isVisible = !isEnabled toggle.setOnClickListener { onToggleSwitch(item.first, toggle.isChecked) } - toggleOverlay.setOnClickListener { - onToggleSwitch(item.first, false) - } title.setOnClickListener { onClickTitle(item.first) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt index c0bdcf0..910385e 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt @@ -23,6 +23,7 @@ import android.os.Bundle import androidx.fragment.app.FragmentActivity import androidx.fragment.app.add import androidx.fragment.app.commit +import foundation.e.privacycentralapp.PrivacyCentralApplication import foundation.e.privacycentralapp.R import foundation.e.privacycentralapp.features.dashboard.DashboardFragment import kotlinx.coroutines.FlowPreview @@ -47,6 +48,11 @@ open class MainActivity : FragmentActivity(R.layout.activity_main) { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) + + (application as? PrivacyCentralApplication) + ?.dependencyContainer?.getQuickPrivacyStateUseCase + ?.resetQuickPrivacyDisabledMessage() + handleIntent(intent) } diff --git a/app/src/main/res/layout/apptrackers_fragment.xml b/app/src/main/res/layout/apptrackers_fragment.xml index 3eb9168..e6e226f 100644 --- a/app/src/main/res/layout/apptrackers_fragment.xml +++ b/app/src/main/res/layout/apptrackers_fragment.xml @@ -60,31 +60,12 @@ android:layout_weight="1" android:text="@string/apptrackers_block_all_toggle" /> - <androidx.constraintlayout.widget.ConstraintLayout + <Switch + android:id="@+id/block_all_toggle" android:layout_width="wrap_content" - android:layout_height="wrap_content"> - - <Switch - android:id="@+id/block_all_toggle" - android:layout_width="wrap_content" - android:layout_height="24dp" - android:checked="true" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - /> - <View - android:id="@+id/block_all_toggle_clicker" - android:layout_width="0dp" - android:layout_height="0dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - android:clickable="true" - /> - </androidx.constraintlayout.widget.ConstraintLayout> + android:layout_height="24dp" + android:checked="true" + /> </LinearLayout> <View android:layout_width="match_parent" diff --git a/app/src/main/res/layout/apptrackers_item_tracker_toggle.xml b/app/src/main/res/layout/apptrackers_item_tracker_toggle.xml index 20e0bdc..8cf7b2e 100644 --- a/app/src/main/res/layout/apptrackers_item_tracker_toggle.xml +++ b/app/src/main/res/layout/apptrackers_item_tracker_toggle.xml @@ -21,28 +21,10 @@ tools:text="Body sensor" /> - <androidx.constraintlayout.widget.ConstraintLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content"> <Switch android:id="@+id/toggle" android:layout_width="wrap_content" android:layout_height="24dp" android:checked="true" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> - <View - android:id="@+id/toggle_clicker" - android:layout_width="0dp" - android:layout_height="0dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - android:clickable="true" - /> - </androidx.constraintlayout.widget.ConstraintLayout> </androidx.appcompat.widget.LinearLayoutCompat> diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index d2cb53c..d79dea1 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -13,7 +13,6 @@ android:layout_width="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" > - <LinearLayout android:background="@color/background" android:gravity="center_horizontal" @@ -200,7 +199,6 @@ android:text="@string/dashboard_state_ipaddress_off" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@+id/graph" /> - <View android:id="@+id/graph_legend_blocked_icon" android:layout_width="16dp" diff --git a/app/src/main/res/layout/fragment_fake_location.xml b/app/src/main/res/layout/fragment_fake_location.xml index 64432cb..47b86bf 100644 --- a/app/src/main/res/layout/fragment_fake_location.xml +++ b/app/src/main/res/layout/fragment_fake_location.xml @@ -9,6 +9,7 @@ android:layout_width="match_parent" > + <include layout="@layout/topbar" /> <androidx.core.widget.NestedScrollView @@ -16,7 +17,6 @@ android:layout_width="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" > - <LinearLayout android:layout_height="match_parent" android:padding="16dp" @@ -24,7 +24,6 @@ android:orientation="vertical" tools:context=".main.MainActivity" > - <TextView android:id="@+id/fake_location_info" android:layout_gravity="center_horizontal" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd9c9bd..489dff2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,7 +7,8 @@ <string name="graph_legend_blocked">Blocked leaks</string> <string name="graph_legend_allowed">Allowed leaks</string> <string name="graph_subtitle">Tap on the bars for more information.</string> - + <string name="quickprivacy_disabled_message">Changes will only be effective when privacy protection toggle is enabled.</string> + <string name="close">Close</string> <!-- Dashboard --> <string name="dashboard_title">@string/app_name</string> |