aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/main/java/com/wireguard/android/preference
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/preference')
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt43
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt72
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt88
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt69
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt135
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt50
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt50
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt30
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt119
9 files changed, 423 insertions, 233 deletions
diff --git a/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
new file mode 100644
index 00000000..2f66a2ca
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.preference
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.util.AttributeSet
+import android.widget.Toast
+import androidx.preference.Preference
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.wireguard.android.R
+import com.wireguard.android.updater.Updater
+import com.wireguard.android.util.ErrorMessages
+import androidx.core.net.toUri
+
+class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ override fun getSummary() = context.getString(R.string.donate_summary)
+
+ override fun getTitle() = context.getString(R.string.donate_title)
+
+ override fun onClick() {
+ /* Google Play Store forbids links to our donation page. */
+ if (Updater.installerIsGooglePlay(context)) {
+ MaterialAlertDialogBuilder(context)
+ .setTitle(R.string.donate_title)
+ .setMessage(R.string.donate_google_play_disappointment)
+ .show()
+ return
+ }
+
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = "https://www.wireguard.com/donations/".toUri()
+ try {
+ context.startActivity(intent)
+ } catch (e: Throwable) {
+ Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt
deleted file mode 100644
index 1479d7b6..00000000
--- a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright © 2020 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-package com.wireguard.android.preference
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.util.AttributeSet
-import androidx.preference.Preference
-import com.wireguard.android.Application
-import com.wireguard.android.R
-import com.wireguard.android.activity.SettingsActivity
-import com.wireguard.android.backend.Tunnel
-import com.wireguard.android.backend.WgQuickBackend
-import java9.util.concurrent.CompletableFuture
-import kotlin.system.exitProcess
-
-class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
- private var state = State.UNKNOWN
-
- init {
- isVisible = false
- Application.getBackendAsync().thenAccept { backend ->
- setState(if (backend is WgQuickBackend) State.ENABLED else State.DISABLED)
- }
- }
-
- override fun getSummary() = if (state == State.UNKNOWN) "" else context.getString(state.summaryResourceId)
-
- 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()
- }
- Application.getAsyncWorker().runAsync {
- Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
- val downings = observableTunnels.map { it.setStateAsync(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
- CompletableFuture.allOf(*downings).thenRun {
- val restartIntent = Intent(context, SettingsActivity::class.java)
- restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- Application.get().startActivity(restartIntent)
- exitProcess(0)
- }
- }.join()
- }
- }
-
- private fun setState(state: State) {
- if (this.state == state) return
- this.state = state
- if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
- if (isVisible != state.visible) isVisible = state.visible
- notifyChanged()
- }
-
- private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean, val visible: Boolean) {
- UNKNOWN(0, 0, false, false),
- ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true, true),
- DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true, true),
- ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false, true),
- DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false, true);
- }
-}
diff --git a/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt
new file mode 100644
index 00000000..3d1c27f1
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.preference
+
+import android.content.Context
+import android.content.Intent
+import android.util.AttributeSet
+import android.util.Log
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import com.wireguard.android.Application
+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.activity
+import com.wireguard.android.util.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.system.exitProcess
+
+class KernelModuleEnablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ private var state = State.UNKNOWN
+
+ init {
+ isVisible = false
+ lifecycleScope.launch {
+ setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
+ }
+ }
+
+ override fun getSummary() = if (state == State.UNKNOWN) "" else context.getString(state.summaryResourceId)
+
+ override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
+
+ override fun onClick() {
+ activity.lifecycleScope.launch {
+ if (state == State.DISABLED) {
+ setState(State.ENABLING)
+ UserKnobs.setEnableKernelModule(true)
+ } else if (state == State.ENABLED) {
+ setState(State.DISABLING)
+ UserKnobs.setEnableKernelModule(false)
+ }
+ val observableTunnels = Application.getTunnelManager().getTunnels()
+ val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
+ try {
+ downings.awaitAll()
+ withContext(Dispatchers.IO) {
+ val restartIntent = Intent(context, SettingsActivity::class.java)
+ restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ Application.get().startActivity(restartIntent)
+ exitProcess(0)
+ }
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ }
+ }
+
+ private fun setState(state: State) {
+ if (this.state == state) return
+ this.state = state
+ if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
+ if (isVisible != state.visible) isVisible = state.visible
+ notifyChanged()
+ }
+
+ private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean, val visible: Boolean) {
+ UNKNOWN(0, 0, false, false),
+ ENABLED(R.string.module_enabler_enabled_title, R.string.module_enabler_enabled_summary, true, true),
+ DISABLED(R.string.module_enabler_disabled_title, R.string.module_enabler_disabled_summary, true, true),
+ ENABLING(R.string.module_enabler_disabled_title, R.string.success_application_will_restart, false, true),
+ DISABLING(R.string.module_enabler_enabled_title, R.string.success_application_will_restart, false, true);
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/KernelModuleEnablerPreference"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt
deleted file mode 100644
index 055ed449..00000000
--- a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright © 2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-package com.wireguard.android.preference
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.system.OsConstants
-import android.util.AttributeSet
-import android.widget.Toast
-import androidx.preference.Preference
-import com.wireguard.android.Application
-import com.wireguard.android.R
-import com.wireguard.android.activity.SettingsActivity
-import com.wireguard.android.util.ErrorMessages
-import kotlin.system.exitProcess
-
-class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
- private var state = State.INITIAL
-
- override fun getSummary() = context.getString(state.messageResourceId)
-
- override fun getTitle() = context.getString(R.string.module_installer_title)
-
- override fun onClick() {
- setState(State.WORKING)
- Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult)
- }
-
- @SuppressLint("ApplySharedPref")
- private fun onDownloadResult(result: Int, throwable: Throwable?) {
- when {
- throwable != null -> {
- setState(State.FAILURE)
- Toast.makeText(context, ErrorMessages[throwable], Toast.LENGTH_LONG).show()
- }
- result == OsConstants.ENOENT -> setState(State.NOTFOUND)
- result == OsConstants.EXIT_SUCCESS -> {
- setState(State.SUCCESS)
- Application.getSharedPreferences().edit().remove("disable_kernel_module").commit()
- Application.getAsyncWorker().runAsync {
- val restartIntent = Intent(context, SettingsActivity::class.java)
- restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- Application.get().startActivity(restartIntent)
- exitProcess(0)
- }
- }
- else -> setState(State.FAILURE)
- }
- }
-
- private fun setState(state: State) {
- if (this.state == state) return
- this.state = state
- if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
- notifyChanged()
- }
-
- private enum class State(val messageResourceId: Int, val shouldEnableView: Boolean) {
- INITIAL(R.string.module_installer_initial, true),
- FAILURE(R.string.module_installer_error, true),
- WORKING(R.string.module_installer_working, false),
- SUCCESS(R.string.success_application_will_restart, false),
- NOTFOUND(R.string.module_installer_not_found, false);
- }
-}
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..e2fc51e3
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.preference
+
+import androidx.datastore.core.DataStore
+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.longPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.core.stringSetPreferencesKey
+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 = stringPreferencesKey(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 = stringSetPreferencesKey(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 = intPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putLong(key: String?, value: Long) {
+ if (key == null) return
+ val pk = longPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putFloat(key: String?, value: Float) {
+ if (key == null) return
+ val pk = floatPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putBoolean(key: String?, value: Boolean) {
+ if (key == null) return
+ val pk = booleanPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun getString(key: String?, defValue: String?): String? {
+ if (key == null) return defValue
+ val pk = stringPreferencesKey(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 = stringSetPreferencesKey(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 = intPreferencesKey(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 = longPreferencesKey(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 = floatPreferencesKey(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 = booleanPreferencesKey(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt b/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt
new file mode 100644
index 00000000..458b9f9a
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.preference
+
+import android.app.StatusBarManager
+import android.content.ComponentName
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.util.AttributeSet
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.preference.Preference
+import com.wireguard.android.QuickTileService
+import com.wireguard.android.R
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class QuickTilePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ override fun getSummary() = context.getString(R.string.quick_settings_tile_add_summary)
+
+ override fun getTitle() = context.getString(R.string.quick_settings_tile_add_title)
+
+ override fun onClick() {
+ val statusBarManager = context.getSystemService(StatusBarManager::class.java)
+ statusBarManager.requestAddTileService(
+ ComponentName(context, QuickTileService::class.java),
+ context.getString(R.string.quick_settings_tile_action),
+ Icon.createWithResource(context, R.drawable.ic_tile),
+ context.mainExecutor
+ ) {
+ when (it) {
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED -> {
+ parent?.removePreference(this)
+ --preferenceManager.preferenceScreen.initialExpandedChildrenCount
+ }
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE ->
+ Toast.makeText(context, context.getString(R.string.quick_settings_tile_add_failure, it), Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt
index f7dd932d..b22048b5 100644
--- a/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
@@ -10,6 +10,10 @@ import androidx.preference.Preference
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.util.ToolsInstaller
+import com.wireguard.android.util.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* Preference implementing a button that asynchronously runs `ToolsInstaller` and displays the
@@ -17,37 +21,41 @@ import com.wireguard.android.util.ToolsInstaller
*/
class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
private var state = State.INITIAL
-
override fun getSummary() = context.getString(state.messageResourceId)
override fun getTitle() = context.getString(R.string.tools_installer_title)
override fun onAttached() {
super.onAttached()
- Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult)
- }
-
- private fun onCheckResult(state: Int, throwable: Throwable?) {
- when {
- throwable != null || state == ToolsInstaller.ERROR -> setState(State.INITIAL)
- state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
- state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
- state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
- else -> setState(State.INITIAL)
+ lifecycleScope.launch {
+ try {
+ val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
+ when {
+ state == ToolsInstaller.ERROR -> setState(State.INITIAL)
+ state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
+ state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
+ state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
+ else -> setState(State.INITIAL)
+ }
+ } catch (_: Throwable) {
+ setState(State.INITIAL)
+ }
}
}
override fun onClick() {
setState(State.WORKING)
- Application.getAsyncWorker().supplyAsync { Application.getToolsInstaller().install() }.whenComplete { result: Int, throwable: Throwable? -> onInstallResult(result, throwable) }
- }
-
- private fun onInstallResult(result: Int, throwable: Throwable?) {
- when {
- throwable != null -> setState(State.FAILURE)
- result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
- result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
- else -> setState(State.FAILURE)
+ lifecycleScope.launch {
+ try {
+ val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
+ when {
+ result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
+ result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
+ else -> setState(State.FAILURE)
+ }
+ } catch (_: Throwable) {
+ setState(State.FAILURE)
+ }
}
}
diff --git a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt
index 0734df45..3850482b 100644
--- a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt
@@ -1,14 +1,14 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
-import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.AttributeSet
+import android.widget.Toast
import androidx.preference.Preference
import com.wireguard.android.Application
import com.wireguard.android.BuildConfig
@@ -16,7 +16,11 @@ import com.wireguard.android.R
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
-import java.util.Locale
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
private var versionSummary: String? = null
@@ -30,7 +34,8 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
intent.data = Uri.parse("https://www.wireguard.com/")
try {
context.startActivity(intent)
- } catch (_: ActivityNotFoundException) {
+ } catch (e: Throwable) {
+ Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
}
}
@@ -43,15 +48,16 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
}
init {
- Application.getBackendAsync().thenAccept { backend ->
- versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
- Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete { version, exception ->
- versionSummary = if (exception == null)
- getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), version)
- else
- getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
- notifyChanged()
+ lifecycleScope.launch {
+ val backend = Application.getBackend()
+ versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).lowercase())
+ notifyChanged()
+ versionSummary = try {
+ getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
+ } catch (_: Throwable) {
+ getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).lowercase())
}
+ notifyChanged()
}
}
}
diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
index cdd25134..52701157 100644
--- a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
@@ -1,24 +1,28 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
-import android.Manifest
import android.content.Context
-import android.content.pm.PackageManager
import android.util.AttributeSet
import android.util.Log
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import com.wireguard.android.Application
import com.wireguard.android.R
-import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.AdminKnobs
import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.DownloadsFileSaver
import com.wireguard.android.util.ErrorMessages
-import com.wireguard.android.util.FragmentUtils
-import java9.util.concurrent.CompletableFuture
+import com.wireguard.android.util.activity
+import com.wireguard.android.util.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.nio.charset.StandardCharsets
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@@ -28,80 +32,77 @@ import java.util.zip.ZipOutputStream
*/
class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
private var exportedFilePath: String? = null
+ private val downloadsFileSaver = DownloadsFileSaver(activity)
private fun exportZip() {
- Application.getTunnelManager().tunnels.thenAccept(this::exportZip)
- }
-
- private fun exportZip(tunnels: List<ObservableTunnel>) {
- val futureConfigs = tunnels.map { it.configAsync.toCompletableFuture() }.toTypedArray()
- if (futureConfigs.isEmpty()) {
- exportZipComplete(null, IllegalArgumentException(
- context.getString(R.string.no_tunnels_error)))
- return
- }
- CompletableFuture.allOf(*futureConfigs)
- .whenComplete { _, exception ->
- Application.getAsyncWorker().supplyAsync {
- if (exception != null) throw exception
- val outputFile = DownloadsFileSaver.save(context, "wireguard-export.zip", "application/zip", true)
- try {
- ZipOutputStream(outputFile.outputStream).use { zip ->
- for (i in futureConfigs.indices) {
- zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
- zip.write(futureConfigs[i].getNow(null)!!.toWgQuickString().toByteArray(StandardCharsets.UTF_8))
- }
- zip.closeEntry()
+ lifecycleScope.launch {
+ val tunnels = Application.getTunnelManager().getTunnels()
+ try {
+ exportedFilePath = withContext(Dispatchers.IO) {
+ val configs = tunnels.map { async(SupervisorJob()) { it.getConfigAsync() } }.awaitAll()
+ if (configs.isEmpty()) {
+ throw IllegalArgumentException(context.getString(R.string.no_tunnels_error))
+ }
+ val outputFile = downloadsFileSaver.save("wireguard-export.zip", "application/zip", true)
+ if (outputFile == null) {
+ withContext(Dispatchers.Main.immediate) {
+ isEnabled = true
+ }
+ return@withContext null
+ }
+ try {
+ ZipOutputStream(outputFile.outputStream).use { zip ->
+ for (i in configs.indices) {
+ zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
+ zip.write(configs[i].toWgQuickString().toByteArray(StandardCharsets.UTF_8))
}
- } catch (e: Exception) {
- outputFile.delete()
- throw e
+ zip.closeEntry()
}
- outputFile.fileName
- }.whenComplete(this::exportZipComplete)
+ } catch (e: Throwable) {
+ outputFile.delete()
+ throw e
+ }
+ outputFile.fileName
}
- }
-
- private fun exportZipComplete(filePath: String?, throwable: Throwable?) {
- if (throwable != null) {
- val error = ErrorMessages[throwable]
- val message = context.getString(R.string.zip_export_error, error)
- Log.e(TAG, message, throwable)
- Snackbar.make(
- FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
- message, Snackbar.LENGTH_LONG).show()
- isEnabled = true
- } else {
- exportedFilePath = filePath
- notifyChanged()
+ notifyChanged()
+ } catch (e: Throwable) {
+ val error = ErrorMessages[e]
+ val message = context.getString(R.string.zip_export_error, error)
+ Log.e(TAG, message, e)
+ Snackbar.make(
+ activity.findViewById(android.R.id.content),
+ message, Snackbar.LENGTH_LONG
+ ).show()
+ isEnabled = true
+ }
}
}
- override fun getSummary() = if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
+ override fun getSummary() =
+ if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
override fun getTitle() = context.getString(R.string.zip_export_title)
override fun onClick() {
- val prefActivity = FragmentUtils.getPrefActivity(this)
- val fragment = prefActivity.supportFragmentManager.fragments.first()
+ if (AdminKnobs.disableConfigExport) return
+ val fragment = activity.supportFragmentManager.fragments.first()
BiometricAuthenticator.authenticate(R.string.biometric_prompt_zip_exporter_title, fragment) {
when (it) {
// When we have successful authentication, or when there is no biometric hardware available.
is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
- prefActivity.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- isEnabled = false
- exportZip()
- }
- }
+ isEnabled = false
+ exportZip()
}
+
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
- prefActivity.findViewById(android.R.id.content),
- it.message,
- Snackbar.LENGTH_SHORT
+ activity.findViewById(android.R.id.content),
+ it.message,
+ Snackbar.LENGTH_SHORT
).show()
}
+
+ is BiometricAuthenticator.Result.Cancelled -> {}
}
}
}