Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2307f5c5 authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Merge branch '2617-prefdatastore' into 'main'

tech:2617: localstate from sharedpref to pref DataStore

See merge request !176
parents ee5386bf dfca84ce
Loading
Loading
Loading
Loading
Loading
+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
@@ -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,
+1 −1
Original line number Diff line number Diff line
@@ -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")
                }
        )
    }
+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
@@ -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)
                }
            }
        }
    }
+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
@@ -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
@@ -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

@@ -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

@@ -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
@@ -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)
@@ -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 {
@@ -155,3 +163,4 @@ class WarningDialog : AppCompatActivity() {
            finish()
        }
    }
}
+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
@@ -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)
@@ -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