From 7d48bef70a56d4370856eedab619b1f83ac3d0d0 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 9 Mar 2020 19:06:11 +0530 Subject: Rename app module to ui Signed-off-by: Harsh Shandilya --- .../wireguard/android/model/ApplicationData.java | 54 ++++ .../wireguard/android/model/ObservableTunnel.java | 142 ++++++++++ .../com/wireguard/android/model/TunnelManager.java | 301 +++++++++++++++++++++ 3 files changed, 497 insertions(+) create mode 100644 ui/src/main/java/com/wireguard/android/model/ApplicationData.java create mode 100644 ui/src/main/java/com/wireguard/android/model/ObservableTunnel.java create mode 100644 ui/src/main/java/com/wireguard/android/model/TunnelManager.java (limited to 'ui/src/main/java/com/wireguard/android/model') diff --git a/ui/src/main/java/com/wireguard/android/model/ApplicationData.java b/ui/src/main/java/com/wireguard/android/model/ApplicationData.java new file mode 100644 index 00000000..65edff90 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/model/ApplicationData.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.model; + +import androidx.databinding.BaseObservable; +import androidx.databinding.Bindable; +import android.graphics.drawable.Drawable; + +import com.wireguard.android.BR; +import com.wireguard.util.Keyed; + +public class ApplicationData extends BaseObservable implements Keyed { + private final Drawable icon; + private final String name; + private final String packageName; + private boolean excludedFromTunnel; + + public ApplicationData(final Drawable icon, final String name, final String packageName, final boolean excludedFromTunnel) { + this.icon = icon; + this.name = name; + this.packageName = packageName; + this.excludedFromTunnel = excludedFromTunnel; + } + + public Drawable getIcon() { + return icon; + } + + @Override + public String getKey() { + return name; + } + + public String getName() { + return name; + } + + public String getPackageName() { + return packageName; + } + + @Bindable + public boolean isExcludedFromTunnel() { + return excludedFromTunnel; + } + + public void setExcludedFromTunnel(final boolean excludedFromTunnel) { + this.excludedFromTunnel = excludedFromTunnel; + notifyPropertyChanged(BR.excludedFromTunnel); + } +} diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.java b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.java new file mode 100644 index 00000000..ce3197f2 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.java @@ -0,0 +1,142 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.model; + +import androidx.databinding.BaseObservable; +import androidx.databinding.Bindable; +import androidx.annotation.Nullable; + +import com.wireguard.android.BR; +import com.wireguard.android.backend.Statistics; +import com.wireguard.android.backend.Tunnel; +import com.wireguard.android.util.ExceptionLoggers; +import com.wireguard.config.Config; +import com.wireguard.util.Keyed; + +import java9.util.concurrent.CompletableFuture; +import java9.util.concurrent.CompletionStage; + +/** + * Encapsulates the volatile and nonvolatile state of a WireGuard tunnel. + */ + +public class ObservableTunnel extends BaseObservable implements Keyed, Tunnel { + private final TunnelManager manager; + @Nullable private Config config; + private State state; + private String name; + @Nullable private Statistics statistics; + + ObservableTunnel(final TunnelManager manager, final String name, + @Nullable final Config config, final State state) { + this.name = name; + this.manager = manager; + this.config = config; + this.state = state; + } + + public CompletionStage delete() { + return manager.delete(this); + } + + @Bindable + @Nullable + public Config getConfig() { + if (config == null) + manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E); + return config; + } + + public CompletionStage getConfigAsync() { + if (config == null) + return manager.getTunnelConfig(this); + return CompletableFuture.completedFuture(config); + } + + @Override + public String getKey() { + return name; + } + + @Override + @Bindable + public String getName() { + return name; + } + + @Bindable + public State getState() { + return state; + } + + public CompletionStage getStateAsync() { + return TunnelManager.getTunnelState(this); + } + + @Bindable + @Nullable + public Statistics getStatistics() { + if (statistics == null || statistics.isStale()) + TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E); + return statistics; + } + + public CompletionStage getStatisticsAsync() { + if (statistics == null || statistics.isStale()) + return TunnelManager.getTunnelStatistics(this); + return CompletableFuture.completedFuture(statistics); + } + + Config onConfigChanged(final Config config) { + this.config = config; + notifyPropertyChanged(BR.config); + return config; + } + + String onNameChanged(final String name) { + this.name = name; + notifyPropertyChanged(BR.name); + return name; + } + + State onStateChanged(final State state) { + if (state != State.UP) + onStatisticsChanged(null); + this.state = state; + notifyPropertyChanged(BR.state); + return state; + } + + @Override + public void onStateChange(final State newState) { + onStateChanged(state); + } + + @Nullable + Statistics onStatisticsChanged(@Nullable final Statistics statistics) { + this.statistics = statistics; + notifyPropertyChanged(BR.statistics); + return statistics; + } + + public CompletionStage setConfig(final Config config) { + if (!config.equals(this.config)) + return manager.setTunnelConfig(this, config); + return CompletableFuture.completedFuture(this.config); + } + + public CompletionStage setName(final String name) { + if (!name.equals(this.name)) + return manager.setTunnelName(this, name); + return CompletableFuture.completedFuture(this.name); + } + + public CompletionStage setState(final State state) { + if (state != this.state) + return manager.setTunnelState(this, state); + return CompletableFuture.completedFuture(this.state); + } +} diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.java b/ui/src/main/java/com/wireguard/android/model/TunnelManager.java new file mode 100644 index 00000000..35d56c81 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.java @@ -0,0 +1,301 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.model; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import androidx.databinding.BaseObservable; +import androidx.databinding.Bindable; +import androidx.annotation.Nullable; + +import com.wireguard.android.Application; +import com.wireguard.android.BR; +import com.wireguard.android.R; +import com.wireguard.android.configStore.ConfigStore; +import com.wireguard.android.backend.Tunnel; +import com.wireguard.android.backend.Tunnel.State; +import com.wireguard.android.backend.Statistics; +import com.wireguard.android.util.ExceptionLoggers; +import com.wireguard.android.util.ObservableSortedKeyedArrayList; +import com.wireguard.android.util.ObservableSortedKeyedList; +import com.wireguard.config.Config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; + +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, + */ + +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)); + } + + 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).apply(); + } + + 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()).apply(); + else + Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply(); + } + + 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); + }); + } + + 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; + } + + /* We disable the below, for now, as the security model of allowing this + * might take a bit more consideration. + */ + if (true) + 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); + }); + } + } +} -- cgit v1.2.3