diff options
5 files changed, 180 insertions, 2 deletions
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"> <uses-permission android:name="android.permission.INTERNET" /> - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <application diff --git a/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java b/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java index e3cd46b1..41761b32 100644 --- a/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java +++ b/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java @@ -1,6 +1,7 @@ package com.wireguard.android.activity; import android.app.Activity; +import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; @@ -9,11 +10,60 @@ import com.wireguard.android.Application; import com.wireguard.android.R; import com.wireguard.android.backend.WgQuickBackend; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + /** * Interface for changing application-global persistent settings. */ public class SettingsActivity extends Activity { + @FunctionalInterface + public interface PermissionRequestCallback { + void done(String[] permissions, int[] grantResults); + } + + private HashMap<Integer, PermissionRequestCallback> 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<String> 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<Tunnel> tunnels = new ArrayList<>(tunnelManager.getTunnels()); + List<CompletableFuture<Config>> 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 @@ <string name="config_save_success">Successfully saved configuration for ā%sā</string> <string name="create_activity_title">Create WireGuard Tunnel</string> <string name="create_empty">Create from scratch</string> - <string name="create_from_file">Create from file</string> + <string name="create_from_file">Create from file or archive</string> <string name="delete">Delete</string> <string name="dns_servers">DNS servers</string> <string name="edit">Edit</string> @@ -33,6 +33,10 @@ <string name="hint_generated">(generated)</string> <string name="hint_optional">(optional)</string> <string name="hint_random">(random)</string> + <string name="zip_exporter_title">Export tunnels to zip file</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="import_error">Unable to import tunnel: %s</string> <string name="import_success">Imported ā%sā</string> <string name="import_total_success">Imported %d tunnels</string> 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" /> <com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" /> + <com.wireguard.android.preference.ZipExporterPreference android:key="zip_exporter" /> </PreferenceScreen> |