From 4fb19dacc9d95c1a9de1fc3a599acd26697c355b Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sat, 12 Oct 2019 13:51:51 +0200 Subject: export: use content resolver on android Q+ --- .../android/preference/LogExporterPreference.java | 44 +++++----- .../android/preference/ZipExporterPreference.java | 19 ++--- .../wireguard/android/util/DownloadsFileSaver.java | 96 ++++++++++++++++++++++ .../java/com/wireguard/config/InetAddresses.java | 2 +- app/src/main/res/values/strings.xml | 2 + 5 files changed, 125 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java diff --git a/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java b/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java index b5988272..565854b4 100644 --- a/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java +++ b/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java @@ -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); } diff --git a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java b/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java index fa5093c6..efda91bb 100644 --- a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java +++ b/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java @@ -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)); } diff --git a/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java b/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java new file mode 100644 index 00000000..efe09f3f --- /dev/null +++ b/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/wireguard/config/InetAddresses.java b/app/src/main/java/com/wireguard/config/InetAddresses.java index 864082e2..6396492e 100644 --- a/app/src/main/java/com/wireguard/config/InetAddresses.java +++ b/app/src/main/java/com/wireguard/config/InetAddresses.java @@ -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); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bed5abc1..7b5dcf70 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,6 +59,7 @@ Create from file or archive Create from QR code Cannot create output directory + Cannot create file in downloads directory Cannot create local temporary directory Create Tunnel Currently using light (day) theme @@ -96,6 +97,7 @@ Saved to “%s” Log file will be saved to downloads folder Export log file + Unable to run logcat: Unable to determine kernel module version MTU Only one userspace tunnel can run at a time -- cgit v1.2.3-59-g8ed1b