Loading app/build.gradle +2 −1 Original line number Diff line number Diff line /* * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * Copyright (C) 2022 - 2024 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 Loading Loading @@ -153,6 +153,7 @@ dependencies { implementation ( libs.androidx.core.ktx, libs.androidx.appcompat, libs.androidx.datastore.preferences, libs.androidx.fragment.ktx, libs.androidx.lifecycle.runtime, libs.androidx.lifecycle.viewmodel, Loading app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +1 −1 Original line number Diff line number Diff line Loading @@ -76,7 +76,7 @@ val appModule = module { SupervisorJob() + Dispatchers.IO + CoroutineExceptionHandler { _, throwable -> Timber.e("Uncaught error in backgroundScope", throwable) Timber.e(throwable, "Uncaught error in backgroundScope") } ) } Loading app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt +9 −4 Original line number Diff line number Diff line /* * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * Copyright (C) 2022 - 2024 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 Loading @@ -22,18 +22,23 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import foundation.e.advancedprivacy.Notifications import foundation.e.advancedprivacy.core.utils.goAsync import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository import kotlinx.coroutines.CoroutineScope import org.koin.java.KoinJavaComponent.inject class BootCompletedReceiver : BroadcastReceiver() { private val localStateRepository by inject<LocalStateRepository>(LocalStateRepository::class.java) private val backgroundScope by inject<CoroutineScope>(CoroutineScope::class.java) override fun onReceive(context: Context, intent: Intent?) { if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { if (localStateRepository.firstBoot) { goAsync(backgroundScope) { if (localStateRepository.isFirstBoot()) { Notifications.showFirstBootNotification(context) localStateRepository.firstBoot = false localStateRepository.setFirstBoot(false) } } } } Loading app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt +32 −23 Original line number Diff line number Diff line /* * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * Copyright (C) 2022 - 2024 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 Loading Loading @@ -29,6 +29,7 @@ import android.widget.CheckBox import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.domain.entities.MainFeatures import foundation.e.advancedprivacy.domain.entities.MainFeatures.FakeLocation Loading @@ -38,8 +39,12 @@ import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase import foundation.e.advancedprivacy.domain.usecases.VpnSupervisorUseCase import foundation.e.advancedprivacy.main.MainActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject import org.koin.java.KoinJavaComponent.get import timber.log.Timber Loading @@ -63,6 +68,9 @@ class WarningDialog : AppCompatActivity() { } } private val showFeaturesWarningUseCase: ShowFeaturesWarningUseCase by inject() private val vpnSupervisorUseCase: VpnSupervisorUseCase by inject() private var isWaitingForResult = false private lateinit var feature: MainFeatures Loading Loading @@ -115,13 +123,14 @@ class WarningDialog : AppCompatActivity() { else -> R.string.ok } ) { _, _ -> if (checkbox.isChecked()) { get<ShowFeaturesWarningUseCase>(ShowFeaturesWarningUseCase::class.java) .doNotShowAgain(feature) lifecycleScope.launch { if (checkbox.isChecked) { withContext(Dispatchers.IO) { showFeaturesWarningUseCase.doNotShowAgain(feature) } } val vpnDisclaimerIntent = (feature as? MainFeatures.IpScrambling) ?.startVpnDisclaimer val vpnDisclaimerIntent = (feature as? IpScrambling)?.startVpnDisclaimer if (vpnDisclaimerIntent != null) { isWaitingForResult = true Loading @@ -130,8 +139,9 @@ class WarningDialog : AppCompatActivity() { finish() } } } if (feature is MainFeatures.TrackersControl) { if (feature is TrackersControl) { builder.setNeutralButton(R.string.warningdialog_trackers_secondary_cta) { _, _ -> MainActivity.deepLinkBuilder(this) .setDestination(R.id.trackersFragment) Loading @@ -144,9 +154,7 @@ class WarningDialog : AppCompatActivity() { } private val launchAndroidVpnDisclaimer = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val vpnSupervisorUseCase = get<VpnSupervisorUseCase>( VpnSupervisorUseCase::class.java ) lifecycleScope.launch { if (result.resultCode == Activity.RESULT_OK) { vpnSupervisorUseCase.startVpnService(feature) } else { Loading @@ -155,3 +163,4 @@ class WarningDialog : AppCompatActivity() { finish() } } } app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepositoryImpl.kt +108 −70 Original line number Diff line number Diff line /* * Copyright (C) 2023-2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * Copyright (C) 2021 - 2024 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 Loading @@ -19,89 +19,111 @@ package foundation.e.advancedprivacy.data.repositories import android.content.Context import androidx.core.content.edit import androidx.datastore.core.DataStore import androidx.datastore.preferences.SharedPreferencesMigration import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import foundation.e.advancedprivacy.core.utils.getValue import foundation.e.advancedprivacy.core.utils.mapKey import foundation.e.advancedprivacy.core.utils.setValue import foundation.e.advancedprivacy.core.utils.toggleValue import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.DisplayableApp import foundation.e.advancedprivacy.domain.entities.FeatureState import foundation.e.advancedprivacy.domain.entities.MainFeatures import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { companion object { private const val SHARED_PREFS_FILE = "localState" private const val KEY_BLOCK_TRACKERS = "blockTrackers" private const val KEY_IP_SCRAMBLING = "ipScrambling" private const val KEY_FAKE_LOCATION = "fakeLocation" private const val KEY_FAKE_LATITUDE = "fakeLatitude" private const val KEY_FAKE_LONGITUDE = "fakeLongitude" private const val KEY_FAKE_LOCATION_WHITELIST = "fakeLocationWhitelist" private const val KEY_FIRST_BOOT = "firstBoot" private const val KEY_HIDE_WARNING_TRACKERS = "hide_warning_trackers" private const val KEY_HIDE_WARNING_LOCATION = "hide_warning_location" private const val KEY_HIDE_WARNING_IPSCRAMBLING = "hide_warning_ipscrambling" private const val KEY_TRACKERS_SCREEN_LAST_POSITION = "trackers_screen_last_position" private const val PREF_DATASTORE = "localstate_datastore" } private val sharedPref = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE) private val blockTrackersKey = booleanPreferencesKey("blockTrackers") private val ipScramblingKey = booleanPreferencesKey("ipScrambling") private val fakeLocationKey = booleanPreferencesKey("fakeLocation") private val fakeLatitudeKey = floatPreferencesKey("fakeLatitude") private val fakeLongitudeKey = floatPreferencesKey("fakeLongitude") private val fakeLocationWhitelistKey = stringSetPreferencesKey("fakeLocationWhitelist") private val firstBootKey = booleanPreferencesKey("firstBoot") private val hideWarningTrackersKey = booleanPreferencesKey("hide_warning_trackers") private val hideWarningLocationKey = booleanPreferencesKey("hide_warning_location") private val hideWarningIpScramblingKey = booleanPreferencesKey("hide_warning_ipscrambling") private val trackersScreenLastPositionKey = intPreferencesKey("trackers_screen_last_position") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore( name = PREF_DATASTORE, produceMigrations = ::sharedPreferencesMigration ) private val store = context.dataStore private val _blockTrackers = MutableStateFlow(sharedPref.getBoolean(KEY_BLOCK_TRACKERS, true)) private fun sharedPreferencesMigration(context: Context) = listOf(SharedPreferencesMigration(context, SHARED_PREFS_FILE)) override val blockTrackers = _blockTrackers.asStateFlow() override val blockTrackers: Flow<Boolean> = store.mapKey(blockTrackersKey, true) override fun setBlockTrackers(enabled: Boolean) { set(KEY_BLOCK_TRACKERS, enabled) _blockTrackers.update { enabled } override suspend fun toggleBlockTrackers(enabled: Boolean?) { store.toggleValue(blockTrackersKey, enabled, true) } override val areAllTrackersBlocked: MutableStateFlow<Boolean> = MutableStateFlow(false) private val _fakeLocationEnabled = MutableStateFlow(sharedPref.getBoolean(KEY_FAKE_LOCATION, false)) override val fakeLocationEnabled = store.mapKey(fakeLocationKey, false) override val fakeLocationEnabled = _fakeLocationEnabled.asStateFlow() override fun setFakeLocationEnabled(enabled: Boolean) { set(KEY_FAKE_LOCATION, enabled) _fakeLocationEnabled.update { enabled } override suspend fun toggleFakeLocation(enabled: Boolean?) { store.toggleValue(fakeLocationKey, enabled, false) } override var fakeLocation: Pair<Float, Float> get() = Pair( override val fakeLocation: Flow<Pair<Float, Float>> = store.data.map { preferences -> // Initial default value is Quezon City sharedPref.getFloat(KEY_FAKE_LATITUDE, 14.6760f), sharedPref.getFloat(KEY_FAKE_LONGITUDE, 121.0437f) ) val lat = preferences[fakeLatitudeKey] ?: 14.6760f val lon = preferences[fakeLongitudeKey] ?: 121.0437f lat to lon } set(value) { sharedPref.edit() .putFloat(KEY_FAKE_LATITUDE, value.first) .putFloat(KEY_FAKE_LONGITUDE, value.second) .apply() override suspend fun getFakeLocation(): Pair<Float, Float> = fakeLocation.first() override suspend fun setFakeLocation(latLon: Pair<Float, Float>) { store.edit { preferences -> preferences[fakeLatitudeKey] = latLon.first preferences[fakeLongitudeKey] = latLon.second } } private val savedWhitelist = sharedPref.getStringSet(KEY_FAKE_LOCATION_WHITELIST, emptySet()) ?: emptySet() private val _fakeLocationWhitelistedApps = MutableStateFlow<Set<String>>(savedWhitelist) override val fakeLocationWhitelistedApps = _fakeLocationWhitelistedApps.asStateFlow() override val fakeLocationWhitelistedApps = store.mapKey(fakeLocationWhitelistKey, emptySet()) override suspend fun toggleAppFakeLocationWhitelisted(app: DisplayableApp) { store.edit { preferences -> val whitelist = preferences[fakeLocationWhitelistKey] ?: emptySet() override suspend fun updateFakeLocationWhitelist(whitelist: Set<String>) { _fakeLocationWhitelistedApps.update { sharedPref.edit(commit = true) { putStringSet(KEY_FAKE_LOCATION_WHITELIST, whitelist) val apIds = app.apps.map { it.apId }.toSet() val appInWhitelist = apIds.any { whitelist.contains(it) } preferences[fakeLocationWhitelistKey] = if (appInWhitelist) { whitelist - apIds } else { whitelist + apIds } whitelist } } private val _ipScramblingSetting = MutableStateFlow(sharedPref.getBoolean(KEY_IP_SCRAMBLING, false)) override val ipScramblingSetting = _ipScramblingSetting.asStateFlow() override suspend fun resetFakeLocationWhitelistedApp() { store.setValue(fakeLocationWhitelistKey, emptySet()) } override fun setIpScramblingSetting(enabled: Boolean) { set(KEY_IP_SCRAMBLING, enabled) _ipScramblingSetting.update { enabled } override val ipScramblingEnabled = store.mapKey(ipScramblingKey, false) override suspend fun toggleIpScrambling(enabled: Boolean?) { store.toggleValue(ipScramblingKey, enabled, false) } override val internetPrivacyMode: MutableStateFlow<FeatureState> = MutableStateFlow(FeatureState.OFF) Loading @@ -122,29 +144,45 @@ class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { override val otherVpnRunning: SharedFlow<ApplicationDescription> = _otherVpnRunning override var firstBoot: Boolean get() = sharedPref.getBoolean(KEY_FIRST_BOOT, true) set(value) = set(KEY_FIRST_BOOT, value) override suspend fun isFirstBoot(): Boolean { return store.getValue(firstBootKey) ?: true } override var hideWarningTrackers: Boolean get() = sharedPref.getBoolean(KEY_HIDE_WARNING_TRACKERS, false) set(value) = set(KEY_HIDE_WARNING_TRACKERS, value) override suspend fun setFirstBoot(isStillFirstBoot: Boolean) { store.setValue(firstBootKey, isStillFirstBoot) } override var hideWarningLocation: Boolean get() = sharedPref.getBoolean(KEY_HIDE_WARNING_LOCATION, false) set(value) = set(KEY_HIDE_WARNING_LOCATION, value) override suspend fun isHideWarningTrackers(): Boolean { return store.getValue(hideWarningTrackersKey) ?: false } override var hideWarningIpScrambling: Boolean get() = sharedPref.getBoolean(KEY_HIDE_WARNING_IPSCRAMBLING, false) set(value) = set(KEY_HIDE_WARNING_IPSCRAMBLING, value) override suspend fun hideWarningTrackers(hide: Boolean) { return store.setValue(hideWarningTrackersKey, hide) } override var trackersScreenLastPosition: Int get() = sharedPref.getInt(KEY_TRACKERS_SCREEN_LAST_POSITION, 0) set(value) = sharedPref.edit().putInt(KEY_TRACKERS_SCREEN_LAST_POSITION, value).apply() override suspend fun isHideWarningLocation(): Boolean { return store.getValue(hideWarningLocationKey) ?: false } override var trackersScreenTabStartPosition: Int = 0 override suspend fun hideWarningLocation(hide: Boolean) { return store.setValue(hideWarningLocationKey, hide) } override suspend fun isHideWarningIpScrambling(): Boolean { return store.getValue(hideWarningIpScramblingKey) ?: false } private fun set(key: String, value: Boolean) { sharedPref.edit().putBoolean(key, value).apply() override suspend fun hideWarningIpScrambling(hide: Boolean) { return store.setValue(hideWarningIpScramblingKey, hide) } override suspend fun getTrackersScreenLastPosition(): Int { return store.getValue(trackersScreenLastPositionKey) ?: 0 } override suspend fun setTrackersScreenLastPosition(position: Int) { store.setValue(trackersScreenLastPositionKey, position) } override var trackersScreenTabStartPosition: Int = 0 } Loading
app/build.gradle +2 −1 Original line number Diff line number Diff line /* * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * Copyright (C) 2022 - 2024 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 Loading Loading @@ -153,6 +153,7 @@ dependencies { implementation ( libs.androidx.core.ktx, libs.androidx.appcompat, libs.androidx.datastore.preferences, libs.androidx.fragment.ktx, libs.androidx.lifecycle.runtime, libs.androidx.lifecycle.viewmodel, Loading
app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +1 −1 Original line number Diff line number Diff line Loading @@ -76,7 +76,7 @@ val appModule = module { SupervisorJob() + Dispatchers.IO + CoroutineExceptionHandler { _, throwable -> Timber.e("Uncaught error in backgroundScope", throwable) Timber.e(throwable, "Uncaught error in backgroundScope") } ) } Loading
app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt +9 −4 Original line number Diff line number Diff line /* * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * Copyright (C) 2022 - 2024 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 Loading @@ -22,18 +22,23 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import foundation.e.advancedprivacy.Notifications import foundation.e.advancedprivacy.core.utils.goAsync import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository import kotlinx.coroutines.CoroutineScope import org.koin.java.KoinJavaComponent.inject class BootCompletedReceiver : BroadcastReceiver() { private val localStateRepository by inject<LocalStateRepository>(LocalStateRepository::class.java) private val backgroundScope by inject<CoroutineScope>(CoroutineScope::class.java) override fun onReceive(context: Context, intent: Intent?) { if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { if (localStateRepository.firstBoot) { goAsync(backgroundScope) { if (localStateRepository.isFirstBoot()) { Notifications.showFirstBootNotification(context) localStateRepository.firstBoot = false localStateRepository.setFirstBoot(false) } } } } Loading
app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt +32 −23 Original line number Diff line number Diff line /* * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * Copyright (C) 2022 - 2024 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 Loading Loading @@ -29,6 +29,7 @@ import android.widget.CheckBox import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.domain.entities.MainFeatures import foundation.e.advancedprivacy.domain.entities.MainFeatures.FakeLocation Loading @@ -38,8 +39,12 @@ import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase import foundation.e.advancedprivacy.domain.usecases.VpnSupervisorUseCase import foundation.e.advancedprivacy.main.MainActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject import org.koin.java.KoinJavaComponent.get import timber.log.Timber Loading @@ -63,6 +68,9 @@ class WarningDialog : AppCompatActivity() { } } private val showFeaturesWarningUseCase: ShowFeaturesWarningUseCase by inject() private val vpnSupervisorUseCase: VpnSupervisorUseCase by inject() private var isWaitingForResult = false private lateinit var feature: MainFeatures Loading Loading @@ -115,13 +123,14 @@ class WarningDialog : AppCompatActivity() { else -> R.string.ok } ) { _, _ -> if (checkbox.isChecked()) { get<ShowFeaturesWarningUseCase>(ShowFeaturesWarningUseCase::class.java) .doNotShowAgain(feature) lifecycleScope.launch { if (checkbox.isChecked) { withContext(Dispatchers.IO) { showFeaturesWarningUseCase.doNotShowAgain(feature) } } val vpnDisclaimerIntent = (feature as? MainFeatures.IpScrambling) ?.startVpnDisclaimer val vpnDisclaimerIntent = (feature as? IpScrambling)?.startVpnDisclaimer if (vpnDisclaimerIntent != null) { isWaitingForResult = true Loading @@ -130,8 +139,9 @@ class WarningDialog : AppCompatActivity() { finish() } } } if (feature is MainFeatures.TrackersControl) { if (feature is TrackersControl) { builder.setNeutralButton(R.string.warningdialog_trackers_secondary_cta) { _, _ -> MainActivity.deepLinkBuilder(this) .setDestination(R.id.trackersFragment) Loading @@ -144,9 +154,7 @@ class WarningDialog : AppCompatActivity() { } private val launchAndroidVpnDisclaimer = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val vpnSupervisorUseCase = get<VpnSupervisorUseCase>( VpnSupervisorUseCase::class.java ) lifecycleScope.launch { if (result.resultCode == Activity.RESULT_OK) { vpnSupervisorUseCase.startVpnService(feature) } else { Loading @@ -155,3 +163,4 @@ class WarningDialog : AppCompatActivity() { finish() } } }
app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepositoryImpl.kt +108 −70 Original line number Diff line number Diff line /* * Copyright (C) 2023-2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * Copyright (C) 2021 - 2024 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 Loading @@ -19,89 +19,111 @@ package foundation.e.advancedprivacy.data.repositories import android.content.Context import androidx.core.content.edit import androidx.datastore.core.DataStore import androidx.datastore.preferences.SharedPreferencesMigration import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import foundation.e.advancedprivacy.core.utils.getValue import foundation.e.advancedprivacy.core.utils.mapKey import foundation.e.advancedprivacy.core.utils.setValue import foundation.e.advancedprivacy.core.utils.toggleValue import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.DisplayableApp import foundation.e.advancedprivacy.domain.entities.FeatureState import foundation.e.advancedprivacy.domain.entities.MainFeatures import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { companion object { private const val SHARED_PREFS_FILE = "localState" private const val KEY_BLOCK_TRACKERS = "blockTrackers" private const val KEY_IP_SCRAMBLING = "ipScrambling" private const val KEY_FAKE_LOCATION = "fakeLocation" private const val KEY_FAKE_LATITUDE = "fakeLatitude" private const val KEY_FAKE_LONGITUDE = "fakeLongitude" private const val KEY_FAKE_LOCATION_WHITELIST = "fakeLocationWhitelist" private const val KEY_FIRST_BOOT = "firstBoot" private const val KEY_HIDE_WARNING_TRACKERS = "hide_warning_trackers" private const val KEY_HIDE_WARNING_LOCATION = "hide_warning_location" private const val KEY_HIDE_WARNING_IPSCRAMBLING = "hide_warning_ipscrambling" private const val KEY_TRACKERS_SCREEN_LAST_POSITION = "trackers_screen_last_position" private const val PREF_DATASTORE = "localstate_datastore" } private val sharedPref = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE) private val blockTrackersKey = booleanPreferencesKey("blockTrackers") private val ipScramblingKey = booleanPreferencesKey("ipScrambling") private val fakeLocationKey = booleanPreferencesKey("fakeLocation") private val fakeLatitudeKey = floatPreferencesKey("fakeLatitude") private val fakeLongitudeKey = floatPreferencesKey("fakeLongitude") private val fakeLocationWhitelistKey = stringSetPreferencesKey("fakeLocationWhitelist") private val firstBootKey = booleanPreferencesKey("firstBoot") private val hideWarningTrackersKey = booleanPreferencesKey("hide_warning_trackers") private val hideWarningLocationKey = booleanPreferencesKey("hide_warning_location") private val hideWarningIpScramblingKey = booleanPreferencesKey("hide_warning_ipscrambling") private val trackersScreenLastPositionKey = intPreferencesKey("trackers_screen_last_position") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore( name = PREF_DATASTORE, produceMigrations = ::sharedPreferencesMigration ) private val store = context.dataStore private val _blockTrackers = MutableStateFlow(sharedPref.getBoolean(KEY_BLOCK_TRACKERS, true)) private fun sharedPreferencesMigration(context: Context) = listOf(SharedPreferencesMigration(context, SHARED_PREFS_FILE)) override val blockTrackers = _blockTrackers.asStateFlow() override val blockTrackers: Flow<Boolean> = store.mapKey(blockTrackersKey, true) override fun setBlockTrackers(enabled: Boolean) { set(KEY_BLOCK_TRACKERS, enabled) _blockTrackers.update { enabled } override suspend fun toggleBlockTrackers(enabled: Boolean?) { store.toggleValue(blockTrackersKey, enabled, true) } override val areAllTrackersBlocked: MutableStateFlow<Boolean> = MutableStateFlow(false) private val _fakeLocationEnabled = MutableStateFlow(sharedPref.getBoolean(KEY_FAKE_LOCATION, false)) override val fakeLocationEnabled = store.mapKey(fakeLocationKey, false) override val fakeLocationEnabled = _fakeLocationEnabled.asStateFlow() override fun setFakeLocationEnabled(enabled: Boolean) { set(KEY_FAKE_LOCATION, enabled) _fakeLocationEnabled.update { enabled } override suspend fun toggleFakeLocation(enabled: Boolean?) { store.toggleValue(fakeLocationKey, enabled, false) } override var fakeLocation: Pair<Float, Float> get() = Pair( override val fakeLocation: Flow<Pair<Float, Float>> = store.data.map { preferences -> // Initial default value is Quezon City sharedPref.getFloat(KEY_FAKE_LATITUDE, 14.6760f), sharedPref.getFloat(KEY_FAKE_LONGITUDE, 121.0437f) ) val lat = preferences[fakeLatitudeKey] ?: 14.6760f val lon = preferences[fakeLongitudeKey] ?: 121.0437f lat to lon } set(value) { sharedPref.edit() .putFloat(KEY_FAKE_LATITUDE, value.first) .putFloat(KEY_FAKE_LONGITUDE, value.second) .apply() override suspend fun getFakeLocation(): Pair<Float, Float> = fakeLocation.first() override suspend fun setFakeLocation(latLon: Pair<Float, Float>) { store.edit { preferences -> preferences[fakeLatitudeKey] = latLon.first preferences[fakeLongitudeKey] = latLon.second } } private val savedWhitelist = sharedPref.getStringSet(KEY_FAKE_LOCATION_WHITELIST, emptySet()) ?: emptySet() private val _fakeLocationWhitelistedApps = MutableStateFlow<Set<String>>(savedWhitelist) override val fakeLocationWhitelistedApps = _fakeLocationWhitelistedApps.asStateFlow() override val fakeLocationWhitelistedApps = store.mapKey(fakeLocationWhitelistKey, emptySet()) override suspend fun toggleAppFakeLocationWhitelisted(app: DisplayableApp) { store.edit { preferences -> val whitelist = preferences[fakeLocationWhitelistKey] ?: emptySet() override suspend fun updateFakeLocationWhitelist(whitelist: Set<String>) { _fakeLocationWhitelistedApps.update { sharedPref.edit(commit = true) { putStringSet(KEY_FAKE_LOCATION_WHITELIST, whitelist) val apIds = app.apps.map { it.apId }.toSet() val appInWhitelist = apIds.any { whitelist.contains(it) } preferences[fakeLocationWhitelistKey] = if (appInWhitelist) { whitelist - apIds } else { whitelist + apIds } whitelist } } private val _ipScramblingSetting = MutableStateFlow(sharedPref.getBoolean(KEY_IP_SCRAMBLING, false)) override val ipScramblingSetting = _ipScramblingSetting.asStateFlow() override suspend fun resetFakeLocationWhitelistedApp() { store.setValue(fakeLocationWhitelistKey, emptySet()) } override fun setIpScramblingSetting(enabled: Boolean) { set(KEY_IP_SCRAMBLING, enabled) _ipScramblingSetting.update { enabled } override val ipScramblingEnabled = store.mapKey(ipScramblingKey, false) override suspend fun toggleIpScrambling(enabled: Boolean?) { store.toggleValue(ipScramblingKey, enabled, false) } override val internetPrivacyMode: MutableStateFlow<FeatureState> = MutableStateFlow(FeatureState.OFF) Loading @@ -122,29 +144,45 @@ class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { override val otherVpnRunning: SharedFlow<ApplicationDescription> = _otherVpnRunning override var firstBoot: Boolean get() = sharedPref.getBoolean(KEY_FIRST_BOOT, true) set(value) = set(KEY_FIRST_BOOT, value) override suspend fun isFirstBoot(): Boolean { return store.getValue(firstBootKey) ?: true } override var hideWarningTrackers: Boolean get() = sharedPref.getBoolean(KEY_HIDE_WARNING_TRACKERS, false) set(value) = set(KEY_HIDE_WARNING_TRACKERS, value) override suspend fun setFirstBoot(isStillFirstBoot: Boolean) { store.setValue(firstBootKey, isStillFirstBoot) } override var hideWarningLocation: Boolean get() = sharedPref.getBoolean(KEY_HIDE_WARNING_LOCATION, false) set(value) = set(KEY_HIDE_WARNING_LOCATION, value) override suspend fun isHideWarningTrackers(): Boolean { return store.getValue(hideWarningTrackersKey) ?: false } override var hideWarningIpScrambling: Boolean get() = sharedPref.getBoolean(KEY_HIDE_WARNING_IPSCRAMBLING, false) set(value) = set(KEY_HIDE_WARNING_IPSCRAMBLING, value) override suspend fun hideWarningTrackers(hide: Boolean) { return store.setValue(hideWarningTrackersKey, hide) } override var trackersScreenLastPosition: Int get() = sharedPref.getInt(KEY_TRACKERS_SCREEN_LAST_POSITION, 0) set(value) = sharedPref.edit().putInt(KEY_TRACKERS_SCREEN_LAST_POSITION, value).apply() override suspend fun isHideWarningLocation(): Boolean { return store.getValue(hideWarningLocationKey) ?: false } override var trackersScreenTabStartPosition: Int = 0 override suspend fun hideWarningLocation(hide: Boolean) { return store.setValue(hideWarningLocationKey, hide) } override suspend fun isHideWarningIpScrambling(): Boolean { return store.getValue(hideWarningIpScramblingKey) ?: false } private fun set(key: String, value: Boolean) { sharedPref.edit().putBoolean(key, value).apply() override suspend fun hideWarningIpScrambling(hide: Boolean) { return store.setValue(hideWarningIpScramblingKey, hide) } override suspend fun getTrackersScreenLastPosition(): Int { return store.getValue(trackersScreenLastPositionKey) ?: 0 } override suspend fun setTrackersScreenLastPosition(position: Int) { store.setValue(trackersScreenLastPositionKey, position) } override var trackersScreenTabStartPosition: Int = 0 }