Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>master
@@ -8,22 +8,21 @@ package com.wireguard.android.preference; | |||
import android.Manifest; | |||
import android.content.Context; | |||
import android.content.pm.PackageManager; | |||
import android.os.Environment; | |||
import androidx.annotation.Nullable; | |||
import com.google.android.material.snackbar.Snackbar; | |||
import androidx.preference.Preference; | |||
import android.util.AttributeSet; | |||
import android.util.Log; | |||
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 java.io.BufferedReader; | |||
import java.io.File; | |||
import java.io.FileOutputStream; | |||
import java.io.IOException; | |||
import java.io.InputStreamReader; | |||
/** | |||
@@ -41,36 +40,33 @@ public class LogExporterPreference extends Preference { | |||
private void exportLog() { | |||
Application.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( | |||
getContext().getString(R.string.create_output_dir_error)); | |||
/* 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(); | |||
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", "-f", file.getAbsolutePath(), "*:V"}); | |||
if (process.waitFor() != 0) { | |||
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { | |||
"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("Unable to run logcat: "); | |||
String line; | |||
while ((line = reader.readLine()) != null) | |||
errors.append(R.string.logcat_error); | |||
while ((line = stderr.readLine()) != null) | |||
errors.append(line); | |||
throw new Exception(errors.toString()); | |||
} | |||
} | |||
} catch (final Exception e) { | |||
// noinspection ResultOfMethodCallIgnored | |||
file.delete(); | |||
outputFile.delete(); | |||
throw e; | |||
} | |||
return file.getAbsolutePath(); | |||
return outputFile.getFileName(); | |||
}).whenComplete(this::exportLogComplete); | |||
} | |||
@@ -8,7 +8,6 @@ package com.wireguard.android.preference; | |||
import android.Manifest; | |||
import android.content.Context; | |||
import android.content.pm.PackageManager; | |||
import android.os.Environment; | |||
import androidx.annotation.Nullable; | |||
import com.google.android.material.snackbar.Snackbar; | |||
import androidx.preference.Preference; | |||
@@ -18,13 +17,12 @@ import android.util.Log; | |||
import com.wireguard.android.Application; | |||
import com.wireguard.android.R; | |||
import com.wireguard.android.model.Tunnel; | |||
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 java.io.File; | |||
import java.io.FileOutputStream; | |||
import java.io.IOException; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
@@ -63,12 +61,8 @@ public class ZipExporterPreference extends Preference { | |||
.whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> { | |||
if (exception != null) | |||
throw exception; | |||
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); | |||
final File file = new File(path, "wireguard-export.zip"); | |||
if (!path.isDirectory() && !path.mkdirs()) | |||
throw new IOException( | |||
getContext().getString(R.string.create_output_dir_error)); | |||
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(file))) { | |||
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). | |||
@@ -76,11 +70,10 @@ public class ZipExporterPreference extends Preference { | |||
} | |||
zip.closeEntry(); | |||
} catch (final Exception e) { | |||
// noinspection ResultOfMethodCallIgnored | |||
file.delete(); | |||
outputFile.delete(); | |||
throw e; | |||
} | |||
return file.getAbsolutePath(); | |||
return outputFile.getFileName(); | |||
}).whenComplete(this::exportZipComplete)); | |||
} | |||
@@ -0,0 +1,96 @@ | |||
/* | |||
* Copyright © 2019 WireGuard LLC. All Rights Reserved. | |||
* SPDX-License-Identifier: Apache-2.0 | |||
*/ | |||
package com.wireguard.android.util; | |||
import android.content.ContentResolver; | |||
import android.content.ContentValues; | |||
import android.content.Context; | |||
import android.database.Cursor; | |||
import android.net.Uri; | |||
import android.os.Build; | |||
import android.os.Environment; | |||
import android.provider.MediaStore; | |||
import android.provider.MediaStore.MediaColumns; | |||
import com.wireguard.android.R; | |||
import java.io.File; | |||
import java.io.FileOutputStream; | |||
import java.io.IOException; | |||
import java.io.OutputStream; | |||
public class DownloadsFileSaver { | |||
public static class DownloadsFile { | |||
private Context context; | |||
private OutputStream outputStream; | |||
private String fileName; | |||
private Uri uri; | |||
private DownloadsFile(final Context context, final OutputStream outputStream, final String fileName, final Uri uri) { | |||
this.context = context; | |||
this.outputStream = outputStream; | |||
this.fileName = fileName; | |||
this.uri = uri; | |||
} | |||
public OutputStream getOutputStream() { return outputStream; } | |||
public String getFileName() { return fileName; } | |||
public void delete() { | |||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) | |||
context.getContentResolver().delete(uri, null, null); | |||
else | |||
new File(fileName).delete(); | |||
} | |||
} | |||
public static DownloadsFile save(final Context context, final String name, final String mimeType, final boolean overwriteExisting) throws Exception { | |||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | |||
final ContentResolver contentResolver = context.getContentResolver(); | |||
if (overwriteExisting) | |||
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), new String[]{name}); | |||
final ContentValues contentValues = new ContentValues(); | |||
contentValues.put(MediaColumns.DISPLAY_NAME, name); | |||
contentValues.put(MediaColumns.MIME_TYPE, mimeType); | |||
final Uri contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues); | |||
if (contentUri == null) | |||
throw new IOException(context.getString(R.string.create_downloads_file_error)); | |||
final OutputStream contentStream = contentResolver.openOutputStream(contentUri); | |||
if (contentStream == null) | |||
throw new IOException(context.getString(R.string.create_downloads_file_error)); | |||
Cursor cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DATA}, null, null, null); | |||
String path = null; | |||
if (cursor != null) { | |||
try { | |||
if (cursor.moveToFirst()) | |||
path = cursor.getString(0); | |||
} finally { | |||
cursor.close(); | |||
} | |||
} | |||
if (path == null) { | |||
path = "Download/"; | |||
cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DISPLAY_NAME}, null, null, null); | |||
if (cursor != null) { | |||
try { | |||
if (cursor.moveToFirst()) | |||
path += cursor.getString(0); | |||
} finally { | |||
cursor.close(); | |||
} | |||
} | |||
} | |||
return new DownloadsFile(context, contentStream, path, contentUri); | |||
} else { | |||
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); | |||
final File file = new File(path, name); | |||
if (!path.isDirectory() && !path.mkdirs()) | |||
throw new IOException(context.getString(R.string.create_output_dir_error)); | |||
return new DownloadsFile(context, new FileOutputStream(file), file.getAbsolutePath(), null); | |||
} | |||
} | |||
} |
@@ -46,7 +46,7 @@ public final class InetAddresses { | |||
if (address.isEmpty()) | |||
throw new ParseException(InetAddress.class, address, "Empty address"); | |||
try { | |||
if (Build.VERSION.SDK_INT < 29) | |||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) | |||
return (InetAddress) getParserMethod().invoke(null, address); | |||
else | |||
return android.net.InetAddresses.parseNumericAddress(address); | |||
@@ -59,6 +59,7 @@ | |||
<string name="create_from_file">Create from file or archive</string> | |||
<string name="create_from_qr_code">Create from QR code</string> | |||
<string name="create_output_dir_error">Cannot create output directory</string> | |||
<string name="create_downloads_file_error">Cannot create file in downloads directory</string> | |||
<string name="create_temp_dir_error">Cannot create local temporary directory</string> | |||
<string name="create_tunnel">Create Tunnel</string> | |||
<string name="dark_theme_summary_off">Currently using light (day) theme</string> | |||
@@ -96,6 +97,7 @@ | |||
<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="log_export_title">Export log file</string> | |||
<string name="logcat_error">Unable to run logcat: </string> | |||
<string name="module_version_error">Unable to determine kernel module version</string> | |||
<string name="mtu">MTU</string> | |||
<string name="multiple_tunnels_error">Only one userspace tunnel can run at a time</string> | |||