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 { @@ -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 = if (config != this.config) + fun setConfigAsync(config: Config): CompletionStage = 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 = 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 COMPARATOR = Comparators.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> completableTunnels = new CompletableFuture<>(); - private final ConfigStore configStore; - private final Context context = Application.get(); - private final ArrayList> delayedLoadRestoreTunnels = new ArrayList<>(); - private final ObservableSortedKeyedList tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR); - private boolean haveLoaded; - @Nullable private ObservableTunnel lastUsedTunnel; - - public TunnelManager(final ConfigStore configStore) { - this.configStore = configStore; - } - - static CompletionStage getTunnelState(final ObservableTunnel tunnel) { - return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel)) - .thenApply(tunnel::onStateChanged); - } - - static CompletionStage 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 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 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 getTunnelConfig(final ObservableTunnel tunnel) { - return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName())) - .thenApply(tunnel::onConfigChanged); - } - - public CompletableFuture> 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 present, final Collection 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[] toComplete; - synchronized (delayedLoadRestoreTunnels) { - haveLoaded = true; - toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]); - delayedLoadRestoreTunnels.clear(); - } - restoreState(true).whenComplete((v, t) -> { - for (final CompletableFuture 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 restoreState(final boolean force) { - if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false)) - return CompletableFuture.completedFuture(null); - synchronized (delayedLoadRestoreTunnels) { - if (!haveLoaded) { - final CompletableFuture f = new CompletableFuture<>(); - delayedLoadRestoreTunnels.add(f); - return f; - } - } - final Set 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 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 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 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 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>() + private val context: Context = get() + private val delayedLoadRestoreTunnels = ArrayList>() + private val tunnelMap: ObservableSortedKeyedList = 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 { + 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 { + 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 = 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, running: Collection) { + 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> + 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 -> 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 { + if (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false)) + return CompletableFuture.completedFuture(null) + synchronized(delayedLoadRestoreTunnels) { + if (!haveLoaded) { + val f = CompletableFuture() + 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 = getAsyncWorker().supplyAsync { + getBackend().setState(tunnel, tunnel.state, config) + configStore.save(tunnel.name, config) + }.thenApply { tunnel.onConfigChanged(it) } + + fun setTunnelName(tunnel: ObservableTunnel, name: String): CompletionStage { + 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.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 = getAsyncWorker() + .supplyAsync { getBackend().getState(tunnel) }.thenApply(tunnel::onStateChanged) + + fun getTunnelStatistics(tunnel: ObservableTunnel): CompletionStage = 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" + } +}