Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>master
@@ -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<CompletableFuture<Tunnel.State>> 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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
}); | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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) { | |||
} | |||
} | |||
} |
@@ -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() | |||
} | |||
} | |||
} | |||
} |
@@ -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<ObservableTunnel> tunnels) { | |||
final List<CompletableFuture<Config>> 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(); | |||
} | |||
}); | |||
} | |||
} |
@@ -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<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() | |||
} | |||
} 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 | |||
} | |||
} |