summaryrefslogtreecommitdiffhomepage
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/src/main/java/com/wireguard/android/Application.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt11
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt6
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt6
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.java305
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.kt239
6 files changed, 252 insertions, 317 deletions
diff --git a/ui/src/main/java/com/wireguard/android/Application.kt b/ui/src/main/java/com/wireguard/android/Application.kt
index 0e522abf..4c57a7ec 100644
--- a/ui/src/main/java/com/wireguard/android/Application.kt
+++ b/ui/src/main/java/com/wireguard/android/Application.kt
@@ -72,7 +72,7 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
}
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
tunnelManager.onCreate()
- asyncWorker.supplyAsync { getBackend() }.thenAccept { futureBackend.complete(it) }
+ asyncWorker.supplyAsync(Companion::getBackend).thenAccept { futureBackend.complete(it) }
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
diff --git a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
index c8d47b40..bda91ae3 100644
--- a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
+++ b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
@@ -24,8 +24,9 @@ class FileConfigStore(private val context: Context) : ConfigStore {
override fun create(name: String, config: Config): Config {
Log.d(TAG, "Creating configuration for tunnel $name")
val file = fileFor(name)
- if (!file.createNewFile()) throw IOException(context.getString(R.string.config_file_exists_error, file.name))
- FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
+ if (!file.createNewFile())
+ throw IOException(context.getString(R.string.config_file_exists_error, file.name))
+ FileOutputStream(file, false).use { it.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
return config
}
@@ -33,7 +34,8 @@ class FileConfigStore(private val context: Context) : ConfigStore {
override fun delete(name: String) {
Log.d(TAG, "Deleting configuration for tunnel $name")
val file = fileFor(name)
- if (!file.delete()) throw IOException(context.getString(R.string.config_delete_error, file.name))
+ if (!file.delete())
+ throw IOException(context.getString(R.string.config_delete_error, file.name))
}
override fun enumerate(): Set<String> {
@@ -68,7 +70,8 @@ class FileConfigStore(private val context: Context) : ConfigStore {
override fun save(name: String, config: Config): Config {
Log.d(TAG, "Saving configuration for tunnel $name")
val file = fileFor(name)
- if (!file.isFile) throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
+ if (!file.isFile)
+ throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
return config
}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
index a6139933..9a92b7cb 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -125,13 +125,11 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
tunnel == null -> {
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
val manager = Application.getTunnelManager()
- manager.create(binding!!.name!!, newConfig)
- .whenComplete(this::onTunnelCreated)
+ manager.create(binding!!.name!!, newConfig).whenComplete(this::onTunnelCreated)
}
tunnel!!.name != binding!!.name -> {
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
- tunnel!!.setNameAsync(binding!!.name!!)
- .whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
+ tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
}
else -> {
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
index 838ad7f9..24b24ed5 100644
--- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
+++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
@@ -77,7 +77,7 @@ class ObservableTunnel internal constructor(
else
CompletableFuture.completedFuture(config)
- fun setConfigAsync(config: Config): CompletionStage<Config?> = if (config != this.config)
+ fun setConfigAsync(config: Config): CompletionStage<Config> = if (config != this.config)
manager.setTunnelConfig(this, config)
else
CompletableFuture.completedFuture(this.config)
@@ -93,13 +93,13 @@ class ObservableTunnel internal constructor(
var statistics: Statistics? = null
get() {
if (field == null || field!!.isStale)
- TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E)
+ manager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E)
return field
}
private set
val statisticsAsync: CompletionStage<Statistics> = if (statistics == null || statistics!!.isStale)
- TunnelManager.getTunnelStatistics(this)
+ manager.getTunnelStatistics(this)
else
CompletableFuture.completedFuture(statistics)
diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.java b/ui/src/main/java/com/wireguard/android/model/TunnelManager.java
deleted file mode 100644
index 547c9344..00000000
--- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.model;
-
-import android.annotation.SuppressLint;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.BR;
-import com.wireguard.android.R;
-import com.wireguard.android.backend.Statistics;
-import com.wireguard.android.backend.Tunnel;
-import com.wireguard.android.backend.Tunnel.State;
-import com.wireguard.android.configStore.ConfigStore;
-import com.wireguard.android.util.ExceptionLoggers;
-import com.wireguard.android.util.ObservableSortedKeyedArrayList;
-import com.wireguard.android.util.ObservableSortedKeyedList;
-import com.wireguard.config.Config;
-import com.wireguard.util.NonNullForAll;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.Set;
-
-import androidx.annotation.Nullable;
-import androidx.databinding.BaseObservable;
-import androidx.databinding.Bindable;
-import java9.util.Comparators;
-import java9.util.concurrent.CompletableFuture;
-import java9.util.concurrent.CompletionStage;
-import java9.util.stream.Collectors;
-import java9.util.stream.StreamSupport;
-
-/**
- * Maintains and mediates changes to the set of available WireGuard tunnels,
- */
-
-@NonNullForAll
-public final class TunnelManager extends BaseObservable {
- private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
- String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
- private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
- private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
- private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
-
- private final CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> completableTunnels = new CompletableFuture<>();
- private final ConfigStore configStore;
- private final Context context = Application.get();
- private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
- private final ObservableSortedKeyedList<String, ObservableTunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
- private boolean haveLoaded;
- @Nullable private ObservableTunnel lastUsedTunnel;
-
- public TunnelManager(final ConfigStore configStore) {
- this.configStore = configStore;
- }
-
- static CompletionStage<State> getTunnelState(final ObservableTunnel tunnel) {
- return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
- .thenApply(tunnel::onStateChanged);
- }
-
- static CompletionStage<Statistics> getTunnelStatistics(final ObservableTunnel tunnel) {
- return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
- .thenApply(tunnel::onStatisticsChanged);
- }
-
- private ObservableTunnel addToList(final String name, @Nullable final Config config, final State state) {
- final ObservableTunnel tunnel = new ObservableTunnel(this, name, config, state);
- tunnels.add(tunnel);
- return tunnel;
- }
-
- public CompletionStage<ObservableTunnel> create(final String name, @Nullable final Config config) {
- if (Tunnel.isNameInvalid(name))
- return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
- if (tunnels.containsKey(name)) {
- final String message = context.getString(R.string.tunnel_error_already_exists, name);
- return CompletableFuture.failedFuture(new IllegalArgumentException(message));
- }
- return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
- .thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN));
- }
-
- CompletionStage<Void> delete(final ObservableTunnel tunnel) {
- final State originalState = tunnel.getState();
- final boolean wasLastUsed = tunnel == lastUsedTunnel;
- // Make sure nothing touches the tunnel.
- if (wasLastUsed)
- setLastUsedTunnel(null);
- tunnels.remove(tunnel);
- return Application.getAsyncWorker().runAsync(() -> {
- if (originalState == State.UP)
- Application.getBackend().setState(tunnel, State.DOWN, null);
- try {
- configStore.delete(tunnel.getName());
- } catch (final Exception e) {
- if (originalState == State.UP)
- Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
- // Re-throw the exception to fail the completion.
- throw e;
- }
- }).whenComplete((x, e) -> {
- if (e == null)
- return;
- // Failure, put the tunnel back.
- tunnels.add(tunnel);
- if (wasLastUsed)
- setLastUsedTunnel(tunnel);
- });
- }
-
- @Bindable
- @Nullable
- public ObservableTunnel getLastUsedTunnel() {
- return lastUsedTunnel;
- }
-
- CompletionStage<Config> getTunnelConfig(final ObservableTunnel tunnel) {
- return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
- .thenApply(tunnel::onConfigChanged);
- }
-
- public CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> getTunnels() {
- return completableTunnels;
- }
-
- public void onCreate() {
- Application.getAsyncWorker().supplyAsync(configStore::enumerate)
- .thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames()), this::onTunnelsLoaded)
- .whenComplete(ExceptionLoggers.E);
- }
-
- @SuppressWarnings("unchecked")
- private void onTunnelsLoaded(final Iterable<String> present, final Collection<String> running) {
- for (final String name : present)
- addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
- final String lastUsedName = Application.getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null);
- if (lastUsedName != null)
- setLastUsedTunnel(tunnels.get(lastUsedName));
- final CompletableFuture<Void>[] toComplete;
- synchronized (delayedLoadRestoreTunnels) {
- haveLoaded = true;
- toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]);
- delayedLoadRestoreTunnels.clear();
- }
- restoreState(true).whenComplete((v, t) -> {
- for (final CompletableFuture<Void> f : toComplete) {
- if (t == null)
- f.complete(v);
- else
- f.completeExceptionally(t);
- }
- });
-
- completableTunnels.complete(tunnels);
- }
-
- public void refreshTunnelStates() {
- Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames())
- .thenAccept(running -> {
- for (final ObservableTunnel tunnel : tunnels)
- tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
- })
- .whenComplete(ExceptionLoggers.E);
- }
-
- public CompletionStage<Void> restoreState(final boolean force) {
- if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
- return CompletableFuture.completedFuture(null);
- synchronized (delayedLoadRestoreTunnels) {
- if (!haveLoaded) {
- final CompletableFuture<Void> f = new CompletableFuture<>();
- delayedLoadRestoreTunnels.add(f);
- return f;
- }
- }
- final Set<String> previouslyRunning = Application.getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null);
- if (previouslyRunning == null)
- return CompletableFuture.completedFuture(null);
- return CompletableFuture.allOf(StreamSupport.stream(tunnels)
- .filter(tunnel -> previouslyRunning.contains(tunnel.getName()))
- .map(tunnel -> setTunnelState(tunnel, State.UP))
- .toArray(CompletableFuture[]::new));
- }
-
- @SuppressLint("ApplySharedPref")
- public void saveState() {
- final Set<String> runningTunnels = StreamSupport.stream(tunnels)
- .filter(tunnel -> tunnel.getState() == State.UP)
- .map(ObservableTunnel::getName)
- .collect(Collectors.toUnmodifiableSet());
- Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).commit();
- }
-
- @SuppressLint("ApplySharedPref")
- private void setLastUsedTunnel(@Nullable final ObservableTunnel tunnel) {
- if (tunnel == lastUsedTunnel)
- return;
- lastUsedTunnel = tunnel;
- notifyPropertyChanged(BR.lastUsedTunnel);
- if (tunnel != null)
- Application.getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).commit();
- else
- Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit();
- }
-
- CompletionStage<Config> setTunnelConfig(final ObservableTunnel tunnel, final Config config) {
- return Application.getAsyncWorker().supplyAsync(() -> {
- Application.getBackend().setState(tunnel, tunnel.getState(), config);
- return configStore.save(tunnel.getName(), config);
- }).thenApply(tunnel::onConfigChanged);
- }
-
- CompletionStage<String> setTunnelName(final ObservableTunnel tunnel, final String name) {
- if (Tunnel.isNameInvalid(name))
- return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
- if (tunnels.containsKey(name)) {
- final String message = context.getString(R.string.tunnel_error_already_exists, name);
- return CompletableFuture.failedFuture(new IllegalArgumentException(message));
- }
- final State originalState = tunnel.getState();
- final boolean wasLastUsed = tunnel == lastUsedTunnel;
- // Make sure nothing touches the tunnel.
- if (wasLastUsed)
- setLastUsedTunnel(null);
- tunnels.remove(tunnel);
- return Application.getAsyncWorker().supplyAsync(() -> {
- if (originalState == State.UP)
- Application.getBackend().setState(tunnel, State.DOWN, null);
- configStore.rename(tunnel.getName(), name);
- final String newName = tunnel.onNameChanged(name);
- if (originalState == State.UP)
- Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
- return newName;
- }).whenComplete((newName, e) -> {
- // On failure, we don't know what state the tunnel might be in. Fix that.
- if (e != null)
- getTunnelState(tunnel);
- // Add the tunnel back to the manager, under whatever name it thinks it has.
- tunnels.add(tunnel);
- if (wasLastUsed)
- setLastUsedTunnel(tunnel);
- });
- }
-
- public CompletionStage<State> setTunnelState(final ObservableTunnel tunnel, final State state) {
- // Ensure the configuration is loaded before trying to use it.
- return tunnel.getConfigAsync().thenCompose(config ->
- Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state, config))
- ).whenComplete((newState, e) -> {
- // Ensure onStateChanged is always called (failure or not), and with the correct state.
- tunnel.onStateChanged(e == null ? newState : tunnel.getState());
- if (e == null && newState == State.UP)
- setLastUsedTunnel(tunnel);
- saveState();
- });
- }
-
- public static final class IntentReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(final Context context, @Nullable final Intent intent) {
- final TunnelManager manager = Application.getTunnelManager();
- if (intent == null)
- return;
- final String action = intent.getAction();
- if (action == null)
- return;
-
- if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES".equals(action)) {
- manager.refreshTunnelStates();
- return;
- }
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
- !Application.getSharedPreferences().getBoolean("allow_remote_control_intents", false))
- return;
-
- final State state;
- if ("com.wireguard.android.action.SET_TUNNEL_UP".equals(action))
- state = State.UP;
- else if ("com.wireguard.android.action.SET_TUNNEL_DOWN".equals(action))
- state = State.DOWN;
- else
- return;
-
- final String tunnelName = intent.getStringExtra("tunnel");
- if (tunnelName == null)
- return;
- manager.getTunnels().thenAccept(tunnels -> {
- final ObservableTunnel tunnel = tunnels.get(tunnelName);
- if (tunnel == null)
- return;
- manager.setTunnelState(tunnel, state);
- });
- }
- }
-}
diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
new file mode 100644
index 00000000..74ecbc66
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.model
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import com.wireguard.android.Application.Companion.get
+import com.wireguard.android.Application.Companion.getAsyncWorker
+import com.wireguard.android.Application.Companion.getBackend
+import com.wireguard.android.Application.Companion.getSharedPreferences
+import com.wireguard.android.Application.Companion.getTunnelManager
+import com.wireguard.android.BR
+import com.wireguard.android.R
+import com.wireguard.android.backend.Statistics
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.configStore.ConfigStore
+import com.wireguard.android.util.ExceptionLoggers
+import com.wireguard.android.util.ObservableSortedKeyedArrayList
+import com.wireguard.android.util.ObservableSortedKeyedList
+import com.wireguard.config.Config
+import java9.util.Comparators
+import java9.util.concurrent.CompletableFuture
+import java9.util.concurrent.CompletionStage
+import java.util.ArrayList
+
+/**
+ * Maintains and mediates changes to the set of available WireGuard tunnels,
+ */
+class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
+ val tunnels = CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>>()
+ private val context: Context = get()
+ private val delayedLoadRestoreTunnels = ArrayList<CompletableFuture<Void>>()
+ private val tunnelMap: ObservableSortedKeyedList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(COMPARATOR)
+ private var haveLoaded = false
+
+ private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel? {
+ val tunnel = ObservableTunnel(this, name, config, state)
+ tunnelMap.add(tunnel)
+ return tunnel
+ }
+
+ fun create(name: String, config: Config?): CompletionStage<ObservableTunnel> {
+ if (Tunnel.isNameInvalid(name))
+ return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
+ if (tunnelMap.containsKey(name))
+ return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
+ return getAsyncWorker().supplyAsync { configStore.create(name, config!!) }.thenApply { addToList(name, it, Tunnel.State.DOWN) }
+ }
+
+ fun delete(tunnel: ObservableTunnel): CompletionStage<Void> {
+ val originalState = tunnel.state
+ val wasLastUsed = tunnel == lastUsedTunnel
+ // Make sure nothing touches the tunnel.
+ if (wasLastUsed)
+ lastUsedTunnel = null
+ tunnelMap.remove(tunnel)
+ return getAsyncWorker().runAsync {
+ if (originalState == Tunnel.State.UP)
+ getBackend().setState(tunnel, Tunnel.State.DOWN, null)
+ try {
+ configStore.delete(tunnel.name)
+ } catch (e: Exception) {
+ if (originalState == Tunnel.State.UP)
+ getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
+ throw e
+ }
+ }.whenComplete { _, e ->
+ if (e == null)
+ return@whenComplete
+ // Failure, put the tunnel back.
+ tunnelMap.add(tunnel)
+ if (wasLastUsed)
+ lastUsedTunnel = tunnel
+ }
+ }
+
+ @get:Bindable
+ @SuppressLint("ApplySharedPref")
+ var lastUsedTunnel: ObservableTunnel? = null
+ private set(value) {
+ if (value == field) return
+ field = value
+ notifyPropertyChanged(BR.lastUsedTunnel)
+ if (value != null)
+ getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, value.name).commit()
+ else
+ getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
+ }
+
+ fun getTunnelConfig(tunnel: ObservableTunnel): CompletionStage<Config> = getAsyncWorker()
+ .supplyAsync { configStore.load(tunnel.name) }.thenApply(tunnel::onConfigChanged)
+
+
+ fun onCreate() {
+ getAsyncWorker().supplyAsync { configStore.enumerate() }
+ .thenAcceptBoth(getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }, this::onTunnelsLoaded)
+ .whenComplete(ExceptionLoggers.E)
+ }
+
+ private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
+ for (name in present)
+ addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
+ val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
+ if (lastUsedName != null)
+ lastUsedTunnel = tunnelMap[lastUsedName]
+ var toComplete: Array<CompletableFuture<Void>>
+ synchronized(delayedLoadRestoreTunnels) {
+ haveLoaded = true
+ toComplete = delayedLoadRestoreTunnels.toTypedArray()
+ delayedLoadRestoreTunnels.clear()
+ }
+ restoreState(true).whenComplete { v: Void?, t: Throwable? ->
+ for (f in toComplete) {
+ if (t == null)
+ f.complete(v)
+ else
+ f.completeExceptionally(t)
+ }
+ }
+ tunnels.complete(tunnelMap)
+ }
+
+ fun refreshTunnelStates() {
+ getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }
+ .thenAccept { running: Set<String> -> for (tunnel in tunnelMap) tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN) }
+ .whenComplete(ExceptionLoggers.E)
+ }
+
+ fun restoreState(force: Boolean): CompletionStage<Void> {
+ if (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
+ return CompletableFuture.completedFuture(null)
+ synchronized(delayedLoadRestoreTunnels) {
+ if (!haveLoaded) {
+ val f = CompletableFuture<Void>()
+ delayedLoadRestoreTunnels.add(f)
+ return f
+ }
+ }
+ val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
+ ?: return CompletableFuture.completedFuture(null)
+ return CompletableFuture.allOf(*tunnelMap.filter { previouslyRunning.contains(it.name) }.map { setTunnelState(it, Tunnel.State.UP).toCompletableFuture() }.toTypedArray())
+ }
+
+ @SuppressLint("ApplySharedPref")
+ fun saveState() {
+ getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet()).commit()
+ }
+
+ fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): CompletionStage<Config> = getAsyncWorker().supplyAsync {
+ getBackend().setState(tunnel, tunnel.state, config)
+ configStore.save(tunnel.name, config)
+ }.thenApply { tunnel.onConfigChanged(it) }
+
+ fun setTunnelName(tunnel: ObservableTunnel, name: String): CompletionStage<String> {
+ if (Tunnel.isNameInvalid(name))
+ return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
+ if (tunnelMap.containsKey(name)) {
+ return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
+ }
+ val originalState = tunnel.state
+ val wasLastUsed = tunnel == lastUsedTunnel
+ // Make sure nothing touches the tunnel.
+ if (wasLastUsed)
+ lastUsedTunnel = null
+ tunnelMap.remove(tunnel)
+ return getAsyncWorker().supplyAsync {
+ if (originalState == Tunnel.State.UP)
+ getBackend().setState(tunnel, Tunnel.State.DOWN, null)
+ configStore.rename(tunnel.name, name)
+ val newName = tunnel.onNameChanged(name)
+ if (originalState == Tunnel.State.UP)
+ getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
+ newName
+ }.whenComplete { _, e ->
+ // On failure, we don't know what state the tunnel might be in. Fix that.
+ if (e != null)
+ getTunnelState(tunnel)
+ // Add the tunnel back to the manager, under whatever name it thinks it has.
+ tunnelMap.add(tunnel)
+ if (wasLastUsed)
+ lastUsedTunnel = tunnel
+ }
+ }
+
+ fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): CompletionStage<Tunnel.State> = tunnel.configAsync
+ .thenCompose { getAsyncWorker().supplyAsync { getBackend().setState(tunnel, state, it) } }
+ .whenComplete { newState, e ->
+ // Ensure onStateChanged is always called (failure or not), and with the correct state.
+ tunnel.onStateChanged(if (e == null) newState else tunnel.state)
+ if (e == null && newState == Tunnel.State.UP)
+ lastUsedTunnel = tunnel
+ saveState()
+ }
+
+ class IntentReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ val manager = getTunnelManager()
+ if (intent == null) return
+ val action = intent.action ?: return
+ if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
+ manager.refreshTunnelStates()
+ return
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !getSharedPreferences().getBoolean("allow_remote_control_intents", false))
+ return
+ val state: Tunnel.State
+ state = when (action) {
+ "com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
+ "com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
+ else -> return
+ }
+ val tunnelName = intent.getStringExtra("tunnel") ?: return
+ manager.tunnels.thenAccept {
+ val tunnel = it[tunnelName] ?: return@thenAccept
+ manager.setTunnelState(tunnel, state)
+ }
+ }
+ }
+
+ fun getTunnelState(tunnel: ObservableTunnel): CompletionStage<Tunnel.State> = getAsyncWorker()
+ .supplyAsync { getBackend().getState(tunnel) }.thenApply(tunnel::onStateChanged)
+
+ fun getTunnelStatistics(tunnel: ObservableTunnel): CompletionStage<Statistics> = getAsyncWorker()
+ .supplyAsync { getBackend().getStatistics(tunnel) }.thenApply(tunnel::onStatisticsChanged)
+
+ companion object {
+ private val COMPARATOR = Comparators.thenComparing(java.lang.String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder())
+ private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
+ private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
+ private const val KEY_RUNNING_TUNNELS = "enabled_configs"
+ }
+}