Preferences: add log exporter

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2018-06-02 18:19:42 +02:00
parent b7e025e381
commit c3246060f5
4 changed files with 137 additions and 18 deletions

View File

@ -0,0 +1,123 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package com.wireguard.android.preference;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.design.widget.Snackbar;
import android.support.v7.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.SettingsActivity;
import com.wireguard.android.util.ExceptionLoggers;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Preference implementing a button that asynchronously exports logs.
*/
public class LogExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
private String exportedFilePath;
public LogExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private static SettingsActivity getPrefActivity(final Preference preference) {
final Context context = preference.getContext();
if (context instanceof ContextThemeWrapper) {
if (((ContextThemeWrapper) context).getBaseContext() instanceof SettingsActivity) {
return ((SettingsActivity) ((ContextThemeWrapper) context).getBaseContext());
}
}
return null;
}
private void exportLog() {
Application.getComponent().getAsyncWorker().supplyAsync(() -> {
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
final File file = new File(path, "wireguard-log.txt");
if (!path.isDirectory() && !path.mkdirs())
throw new IOException("Cannot create output directory");
/* We would like to simply run `builder.redirectOutput(file);`, but this is API 26.
* Instead we have to do this dance, since logcat appends.
*/
new FileOutputStream(file).close();
try {
final Process process = Runtime.getRuntime().exec(new String[]{
"logcat", "-b", "all", "-d", "-v", "threadtime", "--pid", Integer.toString(android.os.Process.myPid()), "-f", file.getAbsolutePath(), "*:V"});
if (process.waitFor() != 0) {
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
final StringBuilder errors = new StringBuilder();
errors.append("Unable to run logcat: ");
String line;
while ((line = reader.readLine()) != null)
errors.append(line);
throw new Exception(errors.toString());
}
}
} catch (Exception e) {
file.delete();
throw e;
}
return file.getAbsolutePath();
}).whenComplete(this::exportLogComplete);
}
private void exportLogComplete(final String filePath, final Throwable throwable) {
if (throwable != null) {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String message = getContext().getString(R.string.log_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
} else {
exportedFilePath = filePath;
setEnabled(false);
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_exporter_title);
}
@Override
protected void onClick() {
getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED)
exportLog();
});
}
}

View File

@ -17,12 +17,9 @@ import android.util.Log;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.Application.ApplicationComponent;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.activity.SettingsActivity; import com.wireguard.android.activity.SettingsActivity;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.AsyncWorker;
import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Config; import com.wireguard.config.Config;
@ -44,16 +41,10 @@ import java9.util.concurrent.CompletableFuture;
public class ZipExporterPreference extends Preference { public class ZipExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName(); private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
private final AsyncWorker asyncWorker;
private final TunnelManager tunnelManager;
private String exportedFilePath; private String exportedFilePath;
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public ZipExporterPreference(final Context context, final AttributeSet attrs) { public ZipExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs); super(context, attrs);
final ApplicationComponent applicationComponent = Application.getComponent();
asyncWorker = applicationComponent.getAsyncWorker();
tunnelManager = applicationComponent.getTunnelManager();
} }
private static SettingsActivity getPrefActivity(final Preference preference) { private static SettingsActivity getPrefActivity(final Preference preference) {
@ -67,7 +58,7 @@ public class ZipExporterPreference extends Preference {
} }
private void exportZip() { private void exportZip() {
final List<Tunnel> tunnels = new ArrayList<>(tunnelManager.getTunnels()); final List<Tunnel> tunnels = new ArrayList<>(Application.getComponent().getTunnelManager().getTunnels());
final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size()); final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
for (final Tunnel tunnel : tunnels) for (final Tunnel tunnel : tunnels)
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture()); futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
@ -76,7 +67,7 @@ public class ZipExporterPreference extends Preference {
return; return;
} }
CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()])) CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
.whenComplete((ignored1, exception) -> asyncWorker.supplyAsync(() -> { .whenComplete((ignored1, exception) -> Application.getComponent().getAsyncWorker().supplyAsync(() -> {
if (exception != null) if (exception != null)
throw exception; throw exception;
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
@ -103,7 +94,7 @@ public class ZipExporterPreference extends Preference {
private void exportZipComplete(final String filePath, final Throwable throwable) { private void exportZipComplete(final String filePath, final Throwable throwable) {
if (throwable != null) { if (throwable != null) {
final String error = ExceptionLoggers.unwrapMessage(throwable); final String error = ExceptionLoggers.unwrapMessage(throwable);
final String message = getContext().getString(R.string.export_error, error); final String message = getContext().getString(R.string.zip_export_error, error);
Log.e(TAG, message, throwable); Log.e(TAG, message, throwable);
Snackbar.make( Snackbar.make(
getPrefActivity(this).findViewById(android.R.id.content), getPrefActivity(this).findViewById(android.R.id.content),
@ -118,8 +109,8 @@ public class ZipExporterPreference extends Preference {
@Override @Override
public CharSequence getSummary() { public CharSequence getSummary() {
return exportedFilePath == null ? return exportedFilePath == null ?
getContext().getString(R.string.export_summary) : getContext().getString(R.string.zip_export_summary) :
getContext().getString(R.string.export_success, exportedFilePath); getContext().getString(R.string.zip_export_success, exportedFilePath);
} }
@Override @Override

View File

@ -39,9 +39,6 @@
<string name="error_down">Error bringing down tunnel: %s</string> <string name="error_down">Error bringing down tunnel: %s</string>
<string name="error_root">Please obtain root access and try again</string> <string name="error_root">Please obtain root access and try again</string>
<string name="error_up">Error bringing up tunnel: %s</string> <string name="error_up">Error bringing up tunnel: %s</string>
<string name="export_error">Unable to export tunnels: %s</string>
<string name="export_success">Saved to %s</string>
<string name="export_summary">Zip file will be saved to downloads folder</string>
<string name="generate">Generate</string> <string name="generate">Generate</string>
<string name="hint_automatic">(auto)</string> <string name="hint_automatic">(auto)</string>
<string name="hint_generated">(generated)</string> <string name="hint_generated">(generated)</string>
@ -51,6 +48,10 @@
<string name="import_success">Imported “%s”</string> <string name="import_success">Imported “%s”</string>
<string name="interface_title">Interface</string> <string name="interface_title">Interface</string>
<string name="listen_port">Listen port</string> <string name="listen_port">Listen port</string>
<string name="log_exporter_title">Export log file</string>
<string name="log_export_error">Unable to export log: %s</string>
<string name="log_export_success">Saved to %s</string>
<string name="log_export_summary">Log file will be saved to downloads folder</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="peer">Peer</string> <string name="peer">Peer</string>
@ -79,4 +80,7 @@
<string name="version_kernel_unknown_summary">Using unknown kernel module implementation</string> <string name="version_kernel_unknown_summary">Using unknown kernel module implementation</string>
<string name="version_userspace_summary">Using Go userspace implementation v%s</string> <string name="version_userspace_summary">Using Go userspace implementation v%s</string>
<string name="zip_exporter_title">Export tunnels to zip file</string> <string name="zip_exporter_title">Export tunnels to zip file</string>
<string name="zip_export_error">Unable to export tunnels: %s</string>
<string name="zip_export_success">Saved to %s</string>
<string name="zip_export_summary">Zip file will be saved to downloads folder</string>
</resources> </resources>

View File

@ -6,7 +6,8 @@
android:summary="@string/restore_on_boot_summary" android:summary="@string/restore_on_boot_summary"
android:title="@string/restore_on_boot_title" /> android:title="@string/restore_on_boot_title" />
<com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" /> <com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" />
<com.wireguard.android.preference.ZipExporterPreference android:key="zip_exporter" /> <com.wireguard.android.preference.ZipExporterPreference />
<com.wireguard.android.preference.LogExporterPreference />
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="dark_theme" android:key="dark_theme"