diff --git a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.java b/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.java deleted file mode 100644 index 885e48b3..00000000 --- a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.java +++ /dev/null @@ -1,94 +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 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.util.NonNullForAll; - -import java.util.ArrayList; -import java.util.Collection; - -import androidx.preference.Preference; -import java9.util.concurrent.CompletableFuture; -import java9.util.stream.Collectors; -import java9.util.stream.StreamSupport; - -@NonNullForAll -public class KernelModuleDisablerPreference extends Preference { - private State state; - - public KernelModuleDisablerPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - state = Application.getBackend() instanceof WgQuickBackend ? State.ENABLED : State.DISABLED; - } - - @Override - public CharSequence getSummary() { - return getContext().getString(state.summaryResourceId); - } - - @Override - public CharSequence getTitle() { - return getContext().getString(state.titleResourceId); - } - - @SuppressLint("ApplySharedPref") - @Override - protected void 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().getTunnels().thenApply(observableTunnels -> { - final Collection> c = StreamSupport.stream(observableTunnels.values()).map(t -> t.setState(Tunnel.State.DOWN).toCompletableFuture()).collect(Collectors.toCollection(ArrayList::new)); - return CompletableFuture.allOf(c.toArray(new CompletableFuture[0])).thenRun(() -> { - final Intent restartIntent = new Intent(getContext(), SettingsActivity.class); - restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Application.get().startActivity(restartIntent); - System.exit(0); - }); - }).join()); - } - - private void setState(final State state) { - if (this.state == state) - return; - this.state = state; - if (isEnabled() != state.shouldEnableView) - setEnabled(state.shouldEnableView); - notifyChanged(); - } - - private enum State { - ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true), - DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true), - ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false), - DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false); - - private final boolean shouldEnableView; - private final int summaryResourceId; - private final int titleResourceId; - - State(final int titleResourceId, final int summaryResourceId, final boolean shouldEnableView) { - this.summaryResourceId = summaryResourceId; - this.titleResourceId = titleResourceId; - this.shouldEnableView = shouldEnableView; - } - } -} diff --git a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt new file mode 100644 index 00000000..0bb728df --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt @@ -0,0 +1,63 @@ +/* + * 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 = if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED + + override fun getSummary() = context.getString(state.summaryResourceId) + + override fun getTitle() = 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.values().map { it.setState(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 + notifyChanged() + } + + private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean) { + ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true), + DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true), + ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false), + DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false); + } +} diff --git a/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.java b/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.java deleted file mode 100644 index 32b3e33c..00000000 --- a/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright © 2017-2019 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 com.google.android.material.snackbar.Snackbar; -import com.wireguard.android.Application; -import com.wireguard.android.R; -import com.wireguard.android.util.DownloadsFileSaver; -import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile; -import com.wireguard.android.util.ErrorMessages; -import com.wireguard.android.util.FragmentUtils; -import com.wireguard.util.NonNullForAll; - -import java.io.BufferedReader; -import java.io.InputStreamReader; - -import androidx.annotation.Nullable; -import androidx.preference.Preference; - -/** - * Preference implementing a button that asynchronously exports logs. - */ - -@NonNullForAll -public class LogExporterPreference extends Preference { - private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName(); - - @Nullable private String exportedFilePath; - - public LogExporterPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - private void exportLog() { - Application.getAsyncWorker().supplyAsync(() -> { - DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-log.txt", "text/plain", true); - try { - final Process process = Runtime.getRuntime().exec(new String[]{ - "logcat", "-b", "all", "-d", "-v", "threadtime", "*:V"}); - try (final BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream())); - final BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { - String line; - while ((line = stdout.readLine()) != null) { - outputFile.getOutputStream().write(line.getBytes()); - outputFile.getOutputStream().write('\n'); - } - outputFile.getOutputStream().close(); - stdout.close(); - if (process.waitFor() != 0) { - final StringBuilder errors = new StringBuilder(); - errors.append(R.string.logcat_error); - while ((line = stderr.readLine()) != null) - errors.append(line); - throw new Exception(errors.toString()); - } - } - } catch (final Exception e) { - outputFile.delete(); - throw e; - } - return outputFile.getFileName(); - }).whenComplete(this::exportLogComplete); - } - - private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) { - if (throwable != null) { - final String error = ErrorMessages.get(throwable); - final String message = getContext().getString(R.string.log_export_error, error); - Log.e(TAG, message, throwable); - Snackbar.make( - FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content), - message, Snackbar.LENGTH_LONG).show(); - setEnabled(true); - } else { - exportedFilePath = filePath; - notifyChanged(); - } - } - - @Override - public CharSequence getSummary() { - return exportedFilePath == null ? - getContext().getString(R.string.log_export_summary) : - getContext().getString(R.string.log_export_success, exportedFilePath); - } - - @Override - public CharSequence getTitle() { - return getContext().getString(R.string.log_export_title); - } - - @Override - protected void onClick() { - FragmentUtils.getPrefActivity(this).ensurePermissions( - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - (permissions, granted) -> { - if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) { - setEnabled(false); - exportLog(); - } - }); - } - -} diff --git a/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.kt new file mode 100644 index 00000000..b4671309 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.kt @@ -0,0 +1,94 @@ +/* + * Copyright © 2017-2019 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.util.DownloadsFileSaver +import com.wireguard.android.util.ErrorMessages +import com.wireguard.android.util.FragmentUtils +import java.io.BufferedReader +import java.io.InputStreamReader + +/** + * Preference implementing a button that asynchronously exports logs. + */ +class LogExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) { + private var exportedFilePath: String? = null + private fun exportLog() { + Application.getAsyncWorker().supplyAsync { + val outputFile = DownloadsFileSaver.save(context, "wireguard-log.txt", "text/plain", true) + try { + val process = Runtime.getRuntime().exec(arrayOf( + "logcat", "-b", "all", "-d", "-v", "threadtime", "*:V")) + BufferedReader(InputStreamReader(process.inputStream)).use { stdout -> + BufferedReader(InputStreamReader(process.errorStream)).use { stderr -> + while (true) { + val line = stdout.readLine() ?: break + outputFile.outputStream.write(line.toByteArray()) + outputFile.outputStream.write('\n'.toInt()) + } + outputFile.outputStream.close() + if (process.waitFor() != 0) { + val errors = StringBuilder() + errors.append(R.string.logcat_error) + while (true) { + val line = stderr.readLine() ?: break + errors.append(line) + } + throw Exception(errors.toString()) + } + } + } + } catch (e: Exception) { + outputFile.delete() + throw e + } + outputFile.fileName + }.whenComplete(this::exportLogComplete) + } + + private fun exportLogComplete(filePath: String, throwable: Throwable?) { + if (throwable != null) { + val error = ErrorMessages.get(throwable) + val message = context.getString(R.string.log_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() + } + } + + override fun getSummary() = if (exportedFilePath == null) + context.getString(R.string.log_export_summary) + else + context.getString(R.string.log_export_success, exportedFilePath) + + override fun getTitle() = context.getString(R.string.log_export_title) + + override fun onClick() { + FragmentUtils.getPrefActivity(this).ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, granted -> + if (granted.isNotEmpty() && granted[0] == PackageManager.PERMISSION_GRANTED) { + isEnabled = false + exportLog() + } + } + } + + companion object { + private val TAG = "WireGuard/" + LogExporterPreference::class.java.simpleName + } +} diff --git a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java b/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java deleted file mode 100644 index 8bea5f8c..00000000 --- a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java +++ /dev/null @@ -1,93 +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 com.wireguard.android.Application; -import com.wireguard.android.R; -import com.wireguard.android.activity.SettingsActivity; -import com.wireguard.android.util.ErrorMessages; -import com.wireguard.util.NonNullForAll; - -import androidx.annotation.Nullable; -import androidx.preference.Preference; - -@NonNullForAll -public class ModuleDownloaderPreference extends Preference { - private State state = State.INITIAL; - - public ModuleDownloaderPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - public CharSequence getSummary() { - return getContext().getString(state.messageResourceId); - } - - @Override - public CharSequence getTitle() { - return getContext().getString(R.string.module_installer_title); - } - - @Override - protected void onClick() { - setState(State.WORKING); - Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult); - } - - @SuppressLint("ApplySharedPref") - private void onDownloadResult(final Integer result, @Nullable final Throwable throwable) { - if (throwable != null) { - setState(State.FAILURE); - Toast.makeText(getContext(), ErrorMessages.get(throwable), Toast.LENGTH_LONG).show(); - } else if (result == OsConstants.ENOENT) - setState(State.NOTFOUND); - else if (result == OsConstants.EXIT_SUCCESS) { - setState(State.SUCCESS); - Application.getSharedPreferences().edit().remove("disable_kernel_module").commit(); - Application.getAsyncWorker().runAsync(() -> { - final Intent restartIntent = new Intent(getContext(), SettingsActivity.class); - restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Application.get().startActivity(restartIntent); - System.exit(0); - }); - } else - setState(State.FAILURE); - } - - private void setState(final State state) { - if (this.state == state) - return; - this.state = state; - if (isEnabled() != state.shouldEnableView) - setEnabled(state.shouldEnableView); - notifyChanged(); - } - - private enum State { - 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); - - private final int messageResourceId; - private final boolean shouldEnableView; - - State(final int messageResourceId, final boolean shouldEnableView) { - this.messageResourceId = messageResourceId; - this.shouldEnableView = shouldEnableView; - } - } -} diff --git a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt new file mode 100644 index 00000000..6ecebb95 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt @@ -0,0 +1,69 @@ +/* + * 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.get(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/ToolsInstallerPreference.java b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java deleted file mode 100644 index 6219ed40..00000000 --- a/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.preference; - -import android.content.Context; -import android.util.AttributeSet; - -import com.wireguard.android.Application; -import com.wireguard.android.R; -import com.wireguard.android.util.ToolsInstaller; -import com.wireguard.util.NonNullForAll; - -import androidx.annotation.Nullable; -import androidx.preference.Preference; - -/** - * Preference implementing a button that asynchronously runs {@code ToolsInstaller} and displays the - * result as the preference summary. - */ - -@NonNullForAll -public class ToolsInstallerPreference extends Preference { - private State state = State.INITIAL; - - public ToolsInstallerPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - public CharSequence getSummary() { - return getContext().getString(state.messageResourceId); - } - - @Override - public CharSequence getTitle() { - return getContext().getString(R.string.tools_installer_title); - } - - @Override - public void onAttached() { - super.onAttached(); - Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult); - } - - private void onCheckResult(final int state, @Nullable final Throwable throwable) { - if (throwable != null || state == ToolsInstaller.ERROR) - setState(State.INITIAL); - else if ((state & ToolsInstaller.YES) == ToolsInstaller.YES) - setState(State.ALREADY); - else if ((state & (ToolsInstaller.MAGISK | ToolsInstaller.NO)) == (ToolsInstaller.MAGISK | ToolsInstaller.NO)) - setState(State.INITIAL_MAGISK); - else if ((state & (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) == (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) - setState(State.INITIAL_SYSTEM); - else - setState(State.INITIAL); - } - - @Override - protected void onClick() { - setState(State.WORKING); - Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult); - } - - private void onInstallResult(final Integer result, @Nullable final Throwable throwable) { - if (throwable != null) - setState(State.FAILURE); - else if ((result & (ToolsInstaller.YES | ToolsInstaller.MAGISK)) == (ToolsInstaller.YES | ToolsInstaller.MAGISK)) - setState(State.SUCCESS_MAGISK); - else if ((result & (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) == (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) - setState(State.SUCCESS_SYSTEM); - else - setState(State.FAILURE); - } - - private void setState(final State state) { - if (this.state == state) - return; - this.state = state; - if (isEnabled() != state.shouldEnableView) - setEnabled(state.shouldEnableView); - notifyChanged(); - } - - private enum State { - INITIAL(R.string.tools_installer_initial, true), - ALREADY(R.string.tools_installer_already, false), - FAILURE(R.string.tools_installer_failure, true), - WORKING(R.string.tools_installer_working, false), - INITIAL_SYSTEM(R.string.tools_installer_initial_system, true), - SUCCESS_SYSTEM(R.string.tools_installer_success_system, false), - INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true), - SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false); - - private final int messageResourceId; - private final boolean shouldEnableView; - - State(final int messageResourceId, final boolean shouldEnableView) { - this.messageResourceId = messageResourceId; - this.shouldEnableView = shouldEnableView; - } - } -} diff --git a/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt new file mode 100644 index 00000000..f7dd932d --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt @@ -0,0 +1,71 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.wireguard.android.preference + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.Preference +import com.wireguard.android.Application +import com.wireguard.android.R +import com.wireguard.android.util.ToolsInstaller + +/** + * Preference implementing a button that asynchronously runs `ToolsInstaller` and displays the + * result as the preference summary. + */ +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) + } + } + + 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) + } + } + + 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.tools_installer_initial, true), + ALREADY(R.string.tools_installer_already, false), + FAILURE(R.string.tools_installer_failure, true), + WORKING(R.string.tools_installer_working, false), + INITIAL_SYSTEM(R.string.tools_installer_initial_system, true), + SUCCESS_SYSTEM(R.string.tools_installer_success_system, false), + INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true), + SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false); + } +} diff --git a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.java b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.java deleted file mode 100644 index b5c7c5e8..00000000 --- a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright © 2017-2019 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 com.wireguard.android.Application; -import com.wireguard.android.BuildConfig; -import com.wireguard.android.R; -import com.wireguard.android.backend.Backend; -import com.wireguard.android.backend.GoBackend; -import com.wireguard.android.backend.WgQuickBackend; -import com.wireguard.util.NonNullForAll; - -import java.util.Locale; - -import androidx.annotation.Nullable; -import androidx.preference.Preference; - -@NonNullForAll -public class VersionPreference extends Preference { - @Nullable private String versionSummary; - - public VersionPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - - 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 = exception == null - ? getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), version) - : getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH)); - notifyChanged(); - }); - }); - } - - private static String getBackendPrettyName(final Context context, final Backend backend) { - if (backend instanceof WgQuickBackend) - return context.getString(R.string.type_name_kernel_module); - if (backend instanceof GoBackend) - return context.getString(R.string.type_name_go_userspace); - return ""; - } - - @Nullable - @Override - public CharSequence getSummary() { - return versionSummary; - } - - @Override - public CharSequence getTitle() { - return getContext().getString(R.string.version_title, BuildConfig.VERSION_NAME); - } - - @Override - protected void onClick() { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("https://www.wireguard.com/")); - try { - getContext().startActivity(intent); - } catch (final ActivityNotFoundException ignored) { - } - } - -} diff --git a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt new file mode 100644 index 00000000..6c5284ec --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt @@ -0,0 +1,57 @@ +/* + * Copyright © 2017-2019 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 androidx.preference.Preference +import com.wireguard.android.Application +import com.wireguard.android.BuildConfig +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 + +class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) { + private var versionSummary: String? = null + + override fun getSummary() = versionSummary + + override fun getTitle() = context.getString(R.string.version_title, BuildConfig.VERSION_NAME) + + override fun onClick() { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse("https://www.wireguard.com/") + try { + context.startActivity(intent) + } catch (_: ActivityNotFoundException) { + } + } + + companion object { + private fun getBackendPrettyName(context: Context, backend: Backend) = when (backend) { + is WgQuickBackend -> context.getString(R.string.type_name_kernel_module) + is GoBackend -> context.getString(R.string.type_name_go_userspace) + else -> "" + } + } + + init { + Application.getBackendAsync().thenAccept { backend: 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() + } + } + } +} diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java deleted file mode 100644 index 0740787f..00000000 --- a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright © 2017-2019 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 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.DownloadsFileSaver; -import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile; -import com.wireguard.android.util.ErrorMessages; -import com.wireguard.android.util.FragmentUtils; -import com.wireguard.config.Config; -import com.wireguard.util.NonNullForAll; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import androidx.annotation.Nullable; -import androidx.preference.Preference; -import java9.util.concurrent.CompletableFuture; - -/** - * Preference implementing a button that asynchronously exports config zips. - */ - -@NonNullForAll -public class ZipExporterPreference extends Preference { - private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName(); - - @Nullable private String exportedFilePath; - - public ZipExporterPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - private void exportZip() { - Application.getTunnelManager().getTunnels().thenAccept(this::exportZip); - } - - private void exportZip(final List tunnels) { - final List> futureConfigs = new ArrayList<>(tunnels.size()); - for (final ObservableTunnel tunnel : tunnels) - futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture()); - if (futureConfigs.isEmpty()) { - exportZipComplete(null, new IllegalArgumentException( - getContext().getString(R.string.no_tunnels_error))); - return; - } - CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()])) - .whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> { - if (exception != null) - throw exception; - DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-export.zip", "application/zip", true); - try (ZipOutputStream zip = new ZipOutputStream(outputFile.getOutputStream())) { - for (int i = 0; i < futureConfigs.size(); ++i) { - zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf")); - zip.write(futureConfigs.get(i).getNow(null). - toWgQuickString().getBytes(StandardCharsets.UTF_8)); - } - zip.closeEntry(); - } catch (final Exception e) { - outputFile.delete(); - throw e; - } - return outputFile.getFileName(); - }).whenComplete(this::exportZipComplete)); - } - - private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) { - if (throwable != null) { - final String error = ErrorMessages.get(throwable); - final String message = getContext().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(); - setEnabled(true); - } else { - exportedFilePath = filePath; - notifyChanged(); - } - } - - @Override - public CharSequence getSummary() { - return exportedFilePath == null ? - getContext().getString(R.string.zip_export_summary) : - getContext().getString(R.string.zip_export_success, exportedFilePath); - } - - @Override - public CharSequence getTitle() { - return getContext().getString(R.string.zip_export_title); - } - - @Override - protected void onClick() { - FragmentUtils.getPrefActivity(this).ensurePermissions( - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - (permissions, granted) -> { - if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) { - setEnabled(false); - exportZip(); - } - }); - } - -} diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt new file mode 100644 index 00000000..2a77f36c --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt @@ -0,0 +1,98 @@ +/* + * Copyright © 2017-2019 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.DownloadsFileSaver +import com.wireguard.android.util.ErrorMessages +import com.wireguard.android.util.FragmentUtils +import com.wireguard.android.util.ObservableSortedKeyedList +import com.wireguard.config.Config +import java9.util.concurrent.CompletableFuture +import java.nio.charset.StandardCharsets +import java.util.ArrayList +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** + * Preference implementing a button that asynchronously exports config zips. + */ +class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) { + private var exportedFilePath: String? = null + + private fun exportZip() { + Application.getTunnelManager().tunnels.thenAccept(this::exportZip) + } + + private fun exportZip(tunnels: List) { + 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() + } + } catch (e: Exception) { + outputFile.delete() + throw e + } + outputFile.fileName + }.whenComplete(this::exportZipComplete) + } + } + + private fun exportZipComplete(filePath: String?, throwable: Throwable?) { + if (throwable != null) { + val error = ErrorMessages.get(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() + } + } + + 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() { + FragmentUtils.getPrefActivity(this).ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, granted -> + if (granted.isNotEmpty() && granted[0] == PackageManager.PERMISSION_GRANTED) { + isEnabled = false + exportZip() + } + } + } + + companion object { + private val TAG = "WireGuard/" + ZipExporterPreference::class.java.simpleName + } +}