summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--build.gradle1
-rw-r--r--ui/build.gradle1
-rw-r--r--ui/src/main/java/com/wireguard/android/Application.kt43
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt35
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.kt84
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt17
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt5
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt132
-rw-r--r--ui/src/main/java/com/wireguard/android/util/UserKnobs.kt85
10 files changed, 304 insertions, 101 deletions
diff --git a/build.gradle b/build.gradle
index 086a6f95..50ff4873 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,6 +11,7 @@ buildscript {
coordinatorLayoutVersion = '1.1.0'
coreKtxVersion = '1.3.1'
coroutinesVersion = '1.3.9'
+ datastoreVersion = '1.0.0-alpha01'
desugarVersion = '1.0.10'
fragmentVersion = '1.3.0-alpha07'
jsr305Version = '3.0.2'
diff --git a/ui/build.gradle b/ui/build.gradle
index b764143d..4c16fb15 100644
--- a/ui/build.gradle
+++ b/ui/build.gradle
@@ -68,6 +68,7 @@ dependencies {
implementation "androidx.fragment:fragment-ktx:$fragmentVersion"
implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleRuntimeKtxVersion"
+ implementation "androidx.datastore:datastore-preferences:$datastoreVersion"
implementation "com.github.material-components:material-components-android:$materialComponentsVersion"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
diff --git a/ui/src/main/java/com/wireguard/android/Application.kt b/ui/src/main/java/com/wireguard/android/Application.kt
index 537d5416..fe98d0d2 100644
--- a/ui/src/main/java/com/wireguard/android/Application.kt
+++ b/ui/src/main/java/com/wireguard/android/Application.kt
@@ -6,15 +6,15 @@ package com.wireguard.android
import android.content.Context
import android.content.Intent
-import android.content.SharedPreferences
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
-import androidx.preference.PreferenceManager
+import androidx.datastore.DataStore
+import androidx.datastore.preferences.Preferences
+import androidx.datastore.preferences.createDataStore
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
@@ -23,22 +23,27 @@ import com.wireguard.android.model.TunnelManager
import com.wireguard.android.util.ModuleLoader
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
+import com.wireguard.android.util.UserKnobs
+import com.wireguard.android.util.applicationScope
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import java.util.Locale
-class Application : android.app.Application(), OnSharedPreferenceChangeListener {
+class Application : android.app.Application() {
private val futureBackend = CompletableDeferred<Backend>()
private val coroutineScope = CoroutineScope(Job() + Dispatchers.Main.immediate)
private var backend: Backend? = null
private lateinit var moduleLoader: ModuleLoader
private lateinit var rootShell: RootShell
- private lateinit var sharedPreferences: SharedPreferences
+ private lateinit var preferencesDataStore: DataStore<Preferences>
private lateinit var toolsInstaller: ToolsInstaller
private lateinit var tunnelManager: TunnelManager
@@ -58,7 +63,7 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
}
}
- private fun determineBackend(): Backend {
+ private suspend fun determineBackend(): Backend {
var backend: Backend? = null
var didStartRootShell = false
if (!ModuleLoader.isModuleLoaded() && moduleLoader.moduleMightExist()) {
@@ -69,19 +74,22 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
} catch (ignored: Exception) {
}
}
- if (!sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) {
+ if (!UserKnobs.disableKernelModule.first() && ModuleLoader.isModuleLoaded()) {
try {
if (!didStartRootShell)
rootShell.start()
val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller)
- wgQuickBackend.setMultipleTunnels(sharedPreferences.getBoolean("multiple_tunnels", false))
+ wgQuickBackend.setMultipleTunnels(UserKnobs.multipleTunnels.first())
backend = wgQuickBackend
+ UserKnobs.multipleTunnels.onEach {
+ wgQuickBackend.setMultipleTunnels(it)
+ }.launchIn(coroutineScope)
} catch (ignored: Exception) {
}
}
if (backend == null) {
backend = GoBackend(applicationContext)
- GoBackend.setAlwaysOnCallback { get().tunnelManager.restoreState(true) }
+ GoBackend.setAlwaysOnCallback { get().applicationScope.launch { get().tunnelManager.restoreState(true) } }
}
return backend
}
@@ -92,10 +100,12 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
rootShell = RootShell(applicationContext)
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT)
- sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+ preferencesDataStore = applicationContext.createDataStore(name = "settings")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- AppCompatDelegate.setDefaultNightMode(
- if (sharedPreferences.getBoolean("dark_theme", false)) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
+ coroutineScope.launch {
+ AppCompatDelegate.setDefaultNightMode(
+ if (UserKnobs.darkTheme.first()) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
+ }
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
@@ -109,16 +119,9 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
Log.e(TAG, Log.getStackTraceString(e))
}
}
- sharedPreferences.registerOnSharedPreferenceChangeListener(this)
- }
-
- override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
- if ("multiple_tunnels" == key && backend != null && backend is WgQuickBackend)
- (backend as WgQuickBackend).setMultipleTunnels(sharedPreferences.getBoolean(key, false))
}
override fun onTerminate() {
- sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
coroutineScope.cancel()
super.onTerminate()
}
@@ -143,7 +146,7 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
fun getRootShell() = get().rootShell
@JvmStatic
- fun getSharedPreferences() = get().sharedPreferences
+ fun getPreferencesDataStore() = get().preferencesDataStore
@JvmStatic
fun getToolsInstaller() = get().toolsInstaller
diff --git a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
index feac43ca..214947d4 100644
--- a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
@@ -14,6 +14,7 @@ import androidx.preference.PreferenceFragmentCompat
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.WgQuickBackend
+import com.wireguard.android.preference.PreferencesPreferenceDataStore
import com.wireguard.android.util.AdminKnobs
import com.wireguard.android.util.ModuleLoader
import kotlinx.coroutines.Dispatchers
@@ -43,6 +44,7 @@ class SettingsActivity : ThemeChangeAwareActivity() {
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
+ preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
addPreferencesFromResource(R.xml.preferences)
preferenceScreen.initialExpandedChildrenCount = 4
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
diff --git a/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt b/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt
index bd124cbc..3ebfbaaa 100644
--- a/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt
@@ -4,39 +4,30 @@
*/
package com.wireguard.android.activity
-import android.content.SharedPreferences
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
-import com.wireguard.android.Application
+import androidx.lifecycle.lifecycleScope
+import com.wireguard.android.util.UserKnobs
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
-abstract class ThemeChangeAwareActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
+abstract class ThemeChangeAwareActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this)
- }
- }
-
- override fun onDestroy() {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this)
- }
- super.onDestroy()
- }
-
- override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
- when (key) {
- "dark_theme" -> {
- AppCompatDelegate.setDefaultNightMode(if (sharedPreferences.getBoolean(key, false)) {
+ UserKnobs.darkTheme.onEach {
+ val newMode = if (it) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
- })
- recreate()
- }
+ }
+ if (AppCompatDelegate.getDefaultNightMode() != newMode) {
+ AppCompatDelegate.setDefaultNightMode(newMode)
+ recreate()
+ }
+ }.launchIn(lifecycleScope)
}
}
}
diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
index f4a0f1b4..fa22b524 100644
--- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
+++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
@@ -4,7 +4,6 @@
*/
package com.wireguard.android.model
-import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -14,7 +13,6 @@ import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import com.wireguard.android.Application.Companion.get
import com.wireguard.android.Application.Companion.getBackend
-import com.wireguard.android.Application.Companion.getSharedPreferences
import com.wireguard.android.Application.Companion.getTunnelManager
import com.wireguard.android.BR
import com.wireguard.android.R
@@ -22,6 +20,7 @@ import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.configStore.ConfigStore
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
+import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.applicationScope
import com.wireguard.config.Config
import kotlinx.coroutines.CompletableDeferred
@@ -29,6 +28,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -84,16 +84,12 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
}
@get:Bindable
- @SuppressLint("ApplySharedPref")
var lastUsedTunnel: ObservableTunnel? = null
private set(value) {
if (value == field) return
field = value
notifyPropertyChanged(BR.lastUsedTunnel)
- if (value != null)
- getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, value.name).commit()
- else
- getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
+ applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) }
}
suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
@@ -113,12 +109,14 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
for (name in present)
addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
- val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
- if (lastUsedName != null)
- lastUsedTunnel = tunnelMap[lastUsedName]
- haveLoaded = true
- restoreState(true)
- tunnels.complete(tunnelMap)
+ applicationScope.launch {
+ val lastUsedName = UserKnobs.lastUsedTunnel.first()
+ if (lastUsedName != null)
+ lastUsedTunnel = tunnelMap[lastUsedName]
+ haveLoaded = true
+ restoreState(true)
+ tunnels.complete(tunnelMap)
+ }
}
private fun refreshTunnelStates() {
@@ -133,26 +131,22 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
}
}
- fun restoreState(force: Boolean) {
- if (!haveLoaded || (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false)))
+ suspend fun restoreState(force: Boolean) {
+ if (!haveLoaded || (!force && !UserKnobs.restoreOnBoot.first()))
return
- val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
- ?: return
+ val previouslyRunning = UserKnobs.runningTunnels.first()
if (previouslyRunning.isEmpty()) return
- applicationScope.launch {
- withContext(Dispatchers.IO) {
- try {
- tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
- } catch (e: Throwable) {
- Log.e(TAG, Log.getStackTraceString(e))
- }
+ withContext(Dispatchers.IO) {
+ try {
+ tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
}
}
}
- @SuppressLint("ApplySharedPref")
- fun saveState() {
- getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet()).commit()
+ suspend fun saveState() {
+ UserKnobs.setRunningTunnels(tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet())
}
suspend fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): Config = withContext(Dispatchers.Main.immediate) {
@@ -216,23 +210,23 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
class IntentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
- val manager = getTunnelManager()
- if (intent == null) return
- val action = intent.action ?: return
- if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
- manager.refreshTunnelStates()
- return
- }
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !getSharedPreferences().getBoolean("allow_remote_control_intents", false))
- return
- val state: Tunnel.State
- state = when (action) {
- "com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
- "com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
- else -> return
- }
- val tunnelName = intent.getStringExtra("tunnel") ?: return
applicationScope.launch {
+ val manager = getTunnelManager()
+ if (intent == null) return@launch
+ val action = intent.action ?: return@launch
+ if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
+ manager.refreshTunnelStates()
+ return@launch
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !UserKnobs.allowRemoteControlIntents.first())
+ return@launch
+ val state: Tunnel.State
+ state = when (action) {
+ "com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
+ "com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
+ else -> return@launch
+ }
+ val tunnelName = intent.getStringExtra("tunnel") ?: return@launch
val tunnels = manager.getTunnels()
val tunnel = tunnels[tunnelName] ?: return@launch
manager.setTunnelState(tunnel, state)
@@ -250,9 +244,5 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
companion object {
private const val TAG = "WireGuard/TunnelManager"
-
- private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
- private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
- private const val KEY_RUNNING_TUNNELS = "enabled_configs"
}
}
diff --git a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt
index 3d47d2ea..5d21a541 100644
--- a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt
@@ -4,7 +4,6 @@
*/
package com.wireguard.android.preference
-import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
@@ -15,6 +14,7 @@ import com.wireguard.android.R
import com.wireguard.android.activity.SettingsActivity
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend
+import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -38,16 +38,15 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
- @SuppressLint("ApplySharedPref")
override fun onClick() {
- if (state == State.DISABLED) {
- setState(State.ENABLING)
- Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", false).commit()
- } else if (state == State.ENABLED) {
- setState(State.DISABLING)
- Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", true).commit()
- }
lifecycleScope.launch {
+ if (state == State.DISABLED) {
+ setState(State.ENABLING)
+ UserKnobs.setDisableKernelModule(false)
+ } else if (state == State.ENABLED) {
+ setState(State.DISABLING)
+ UserKnobs.setDisableKernelModule(true)
+ }
val observableTunnels = Application.getTunnelManager().getTunnels()
val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
try {
diff --git a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt
index 7e437982..5ba2c4f0 100644
--- a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt
@@ -4,7 +4,6 @@
*/
package com.wireguard.android.preference
-import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.system.OsConstants
@@ -15,6 +14,7 @@ import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.activity.SettingsActivity
import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -27,7 +27,6 @@ class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Prefe
override fun getTitle() = context.getString(R.string.module_installer_title)
- @SuppressLint("ApplySharedPref")
override fun onClick() {
setState(State.WORKING)
lifecycleScope.launch {
@@ -36,7 +35,7 @@ class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Prefe
OsConstants.ENOENT -> setState(State.NOTFOUND)
OsConstants.EXIT_SUCCESS -> {
setState(State.SUCCESS)
- Application.getSharedPreferences().edit().remove("disable_kernel_module").commit()
+ UserKnobs.setDisableKernelModule(null)
withContext(Dispatchers.IO) {
val restartIntent = Intent(context, SettingsActivity::class.java)
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
diff --git a/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt b/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt
new file mode 100644
index 00000000..96ca2b1c
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright © 2020 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.preference
+
+import androidx.datastore.DataStore
+import androidx.datastore.preferences.Preferences
+import androidx.datastore.preferences.edit
+import androidx.datastore.preferences.preferencesKey
+import androidx.datastore.preferences.preferencesSetKey
+import androidx.datastore.preferences.remove
+import androidx.preference.PreferenceDataStore
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+
+class PreferencesPreferenceDataStore(private val coroutineScope: CoroutineScope, private val dataStore: DataStore<Preferences>) : PreferenceDataStore() {
+ override fun putString(key: String?, value: String?) {
+ if (key == null) return
+ val pk = preferencesKey<String>(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ if (value == null) it.remove(pk)
+ else it[pk] = value
+ }
+ }
+ }
+
+ override fun putStringSet(key: String?, values: Set<String?>?) {
+ if (key == null) return
+ val pk = preferencesSetKey<String>(key)
+ val filteredValues = values?.filterNotNull()?.toSet()
+ coroutineScope.launch {
+ dataStore.edit {
+ if (filteredValues == null || filteredValues.isEmpty()) it.remove(pk)
+ else it[pk] = filteredValues
+ }
+ }
+ }
+
+ override fun putInt(key: String?, value: Int) {
+ if (key == null) return
+ val pk = preferencesKey<Int>(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putLong(key: String?, value: Long) {
+ if (key == null) return
+ val pk = preferencesKey<Long>(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putFloat(key: String?, value: Float) {
+ if (key == null) return
+ val pk = preferencesKey<Float>(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putBoolean(key: String?, value: Boolean) {
+ if (key == null) return
+ val pk = preferencesKey<Boolean>(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun getString(key: String?, defValue: String?): String? {
+ if (key == null) return defValue
+ val pk = preferencesKey<String>(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+
+ override fun getStringSet(key: String?, defValues: Set<String?>?): Set<String?>? {
+ if (key == null) return defValues
+ val pk = preferencesSetKey<String>(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValues }.first()
+ }
+ }
+
+ override fun getInt(key: String?, defValue: Int): Int {
+ if (key == null) return defValue
+ val pk = preferencesKey<Int>(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+
+ override fun getLong(key: String?, defValue: Long): Long {
+ if (key == null) return defValue
+ val pk = preferencesKey<Long>(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+
+ override fun getFloat(key: String?, defValue: Float): Float {
+ if (key == null) return defValue
+ val pk = preferencesKey<Float>(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+
+ override fun getBoolean(key: String?, defValue: Boolean): Boolean {
+ if (key == null) return defValue
+ val pk = preferencesKey<Boolean>(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt b/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
new file mode 100644
index 00000000..b0107ebf
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2020 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util
+
+import androidx.datastore.preferences.edit
+import androidx.datastore.preferences.preferencesKey
+import androidx.datastore.preferences.preferencesSetKey
+import androidx.datastore.preferences.remove
+import com.wireguard.android.Application
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+object UserKnobs {
+ private val DISABLE_KERNEL_MODULE = preferencesKey<Boolean>("disable_kernel_module")
+ val disableKernelModule: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[DISABLE_KERNEL_MODULE] ?: false
+ }
+
+ suspend fun setDisableKernelModule(disable: Boolean?) {
+ Application.getPreferencesDataStore().edit {
+ if (disable == null)
+ it.remove(DISABLE_KERNEL_MODULE)
+ else
+ it[DISABLE_KERNEL_MODULE] = disable
+ }
+ }
+
+ private val MULTIPLE_TUNNELS = preferencesKey<Boolean>("multiple_tunnels")
+ val multipleTunnels: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[MULTIPLE_TUNNELS] ?: false
+ }
+
+ private val DARK_THEME = preferencesKey<Boolean>("dark_theme")
+ val darkTheme: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[DARK_THEME] ?: false
+ }
+
+ private val ALLOW_REMOTE_CONTROL_INTENTS = preferencesKey<Boolean>("allow_remote_control_intents")
+ val allowRemoteControlIntents: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[ALLOW_REMOTE_CONTROL_INTENTS] ?: false
+ }
+
+ private val RESTORE_ON_BOOT = preferencesKey<Boolean>("restore_on_boot")
+ val restoreOnBoot: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[RESTORE_ON_BOOT] ?: false
+ }
+
+ private val LAST_USED_TUNNEL = preferencesKey<String>("last_used_tunnel")
+ val lastUsedTunnel: Flow<String?>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[LAST_USED_TUNNEL]
+ }
+
+ suspend fun setLastUsedTunnel(lastUsedTunnel: String?) {
+ Application.getPreferencesDataStore().edit {
+ if (lastUsedTunnel == null)
+ it.remove(LAST_USED_TUNNEL)
+ else
+ it[LAST_USED_TUNNEL] = lastUsedTunnel
+ }
+ }
+
+ private val RUNNING_TUNNELS = preferencesSetKey<String>("enabled_configs")
+ val runningTunnels: Flow<Set<String>>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[RUNNING_TUNNELS] ?: emptySet()
+ }
+
+ suspend fun setRunningTunnels(runningTunnels: Set<String>) {
+ Application.getPreferencesDataStore().edit {
+ if (runningTunnels.isEmpty())
+ it.remove(RUNNING_TUNNELS)
+ else
+ it[RUNNING_TUNNELS] = runningTunnels
+ }
+ }
+}