From 622f41f11f92005e2dd3791fd13b0ace294958d5 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 29 Apr 2018 02:04:28 +0200 Subject: [PATCH] Allow exporting to zip file Signed-off-by: Jason A. Donenfeld --- app/src/main/AndroidManifest.xml | 2 +- .../android/activity/SettingsActivity.java | 50 +++++++ .../preference/ZipExporterPreference.java | 123 ++++++++++++++++++ app/src/main/res/values/strings.xml | 6 +- app/src/main/res/xml/preferences.xml | 1 + 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8cd0f623..63d8aa78 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ android:installLocation="internalOnly"> - + permissionRequestCallbacks = new HashMap<>(); + private int permissionRequestCounter = 0; + + public synchronized void ensurePermissions(String[] permissions, PermissionRequestCallback cb) { + /* TODO(MSF): since when porting to AppCompat, you'll be replacing checkSelfPermission + * and requestPermission with AppCompat.checkSelfPermission and AppCompat.requestPermission, + * you can remove this SDK_INT block entirely here, and count on the compat lib to do + * the right thing. */ + if (android.os.Build.VERSION.SDK_INT < 23) { + int[] granted = new int[permissions.length]; + Arrays.fill(granted, PackageManager.PERMISSION_GRANTED); + cb.done(permissions, granted); + } else { + List needPermissions = new ArrayList<>(permissions.length); + for (final String permission : permissions) { + if (getApplicationContext().checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) + needPermissions.add(permission); + } + if (needPermissions.isEmpty()) { + int[] granted = new int[permissions.length]; + Arrays.fill(granted, PackageManager.PERMISSION_GRANTED); + cb.done(permissions, granted); + return; + } + int idx = permissionRequestCounter++; + permissionRequestCallbacks.put(idx, cb); + requestPermissions(needPermissions.toArray(new String[needPermissions.size()]), idx); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode); + if (f != null) { + permissionRequestCallbacks.remove(requestCode); + f.done(permissions, grantResults); + } + } + @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java b/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java new file mode 100644 index 00000000..2101420f --- /dev/null +++ b/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java @@ -0,0 +1,123 @@ +package com.wireguard.android.preference; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.preference.Preference; +import android.util.AttributeSet; +import android.util.Log; + +import com.commonsware.cwac.crossport.design.widget.Snackbar; +import com.wireguard.android.Application; +import com.wireguard.android.Application.ApplicationComponent; +import com.wireguard.android.R; +import com.wireguard.android.activity.SettingsActivity; +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.config.Config; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import java9.util.concurrent.CompletableFuture; + +/** + * Preference implementing a button that asynchronously exports config zips. + */ + +public class ZipExporterPreference extends Preference { + private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName(); + + private final AsyncWorker asyncWorker; + private final TunnelManager tunnelManager; + private String exportedFilePath = null; + + @SuppressWarnings({"SameParameterValue", "WeakerAccess"}) + public ZipExporterPreference(final Context context, final AttributeSet attrs) { + super(context, attrs); + final ApplicationComponent applicationComponent = Application.getComponent(); + asyncWorker = applicationComponent.getAsyncWorker(); + tunnelManager = applicationComponent.getTunnelManager(); + } + + @Override + public CharSequence getSummary() { + if (exportedFilePath == null) + return getContext().getString(R.string.export_summary); + else + return getContext().getString(R.string.export_success, exportedFilePath); + } + + @Override + public CharSequence getTitle() { + return getContext().getString(getTitleRes()); + } + + @Override + public int getTitleRes() { + return R.string.zip_exporter_title; + } + + private void exportZip() { + List tunnels = new ArrayList<>(tunnelManager.getTunnels()); + List> futureConfigs = new ArrayList<>(tunnels.size()); + for (final Tunnel tunnel : tunnels) + futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture()); + if (futureConfigs.isEmpty()) { + exportZipComplete(null, new IllegalArgumentException("No tunnels exist")); + return; + } + CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()])) + .whenComplete((ignored1, exception) -> { + asyncWorker.supplyAsync(() -> { + if (exception != null) + throw exception; + final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + final File file = new File(path, "wireguard-export.zip"); + try { + path.mkdirs(); + final ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(file)); + for (int i = 0; i < futureConfigs.size(); ++i) { + zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf")); + zip.write(futureConfigs.get(i).getNow(null).toString().getBytes(StandardCharsets.UTF_8)); + } + zip.closeEntry(); + zip.close(); + } catch (Exception e) { + file.delete(); + throw e; + } + return file.getAbsolutePath(); + }).whenComplete(this::exportZipComplete); + }); + } + + private void exportZipComplete(String filePath, Throwable throwable) { + if (throwable != null) { + final String error = ExceptionLoggers.unwrap(throwable).getMessage(); + final String message = getContext().getString(R.string.export_error, error); + Log.e(TAG, message, throwable); + Snackbar.make(((SettingsActivity)getContext()).findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show(); + } else { + exportedFilePath = filePath; + setEnabled(false); + notifyChanged(); + } + } + + @Override + protected void onClick() { + ((SettingsActivity)getContext()).ensurePermissions(new String[] { "android.permission.WRITE_EXTERNAL_STORAGE" }, (permissions, granted) -> { + if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) + exportZip(); + }); + } + +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3eb72f5b..9aeb290f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ Successfully saved configuration for ā€œ%sā€ Create WireGuard Tunnel Create from scratch - Create from file + Create from file or archive Delete DNS servers Edit @@ -33,6 +33,10 @@ (generated) (optional) (random) + Export tunnels to zip file + Unable to export tunnels: %s + Saved to %s + Zip file will be saved to downloads folder Unable to import tunnel: %s Imported ā€œ%sā€ Imported %d tunnels diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b032bea7..c73c174b 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -6,4 +6,5 @@ android:summary="@string/restore_on_boot_summary" android:title="@string/restore_on_boot_title" /> +