summaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt4
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/common/QuickPrivacyDisabledSnackbar.kt36
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt8
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt59
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt24
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt14
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt7
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt16
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt21
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt50
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt11
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt85
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt65
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFeature.kt18
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt10
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt10
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFeature.kt27
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt19
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt6
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt6
-rw-r--r--app/src/main/res/layout/apptrackers_fragment.xml29
-rw-r--r--app/src/main/res/layout/apptrackers_item_tracker_toggle.xml18
-rw-r--r--app/src/main/res/layout/fragment_dashboard.xml2
-rw-r--r--app/src/main/res/layout/fragment_fake_location.xml3
-rw-r--r--app/src/main/res/values/strings.xml3
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>