diff options
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/preference')
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 -> {} } } } |