diff options
author | Samuel Holland <samuel@sholland.org> | 2018-01-01 02:06:37 -0600 |
---|---|---|
committer | Samuel Holland <samuel@sholland.org> | 2018-01-06 04:09:29 -0600 |
commit | 609194fae2332e6f2ccd7a4464bfa492ad661a6f (patch) | |
tree | 96a7cd9846a093dfcdacfef285b0a4d77000edf0 /app/src/main/java/com/wireguard/android/model | |
parent | Rename package widgets -> widget (diff) | |
download | wireguard-android-609194fae2332e6f2ccd7a4464bfa492ad661a6f.tar.xz wireguard-android-609194fae2332e6f2ccd7a4464bfa492ad661a6f.zip |
Serviceless rewrite, part 1
Signed-off-by: Samuel Holland <samuel@sholland.org>
Diffstat (limited to 'app/src/main/java/com/wireguard/android/model')
3 files changed, 287 insertions, 0 deletions
diff --git a/app/src/main/java/com/wireguard/android/model/Tunnel.java b/app/src/main/java/com/wireguard/android/model/Tunnel.java new file mode 100644 index 00000000..b196eaa5 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/model/Tunnel.java @@ -0,0 +1,166 @@ +package com.wireguard.android.model; + +import android.databinding.BaseObservable; +import android.databinding.Bindable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.wireguard.android.BR; +import com.wireguard.android.backend.Backend; +import com.wireguard.android.configStore.ConfigStore; +import com.wireguard.android.util.ExceptionLoggers; +import com.wireguard.config.Config; + +import org.threeten.bp.Instant; + +import java.util.Objects; +import java.util.regex.Pattern; + +import java9.util.concurrent.CompletableFuture; +import java9.util.concurrent.CompletionStage; + +/** + * Encapsulates the volatile and nonvolatile state of a WireGuard tunnel. + */ + +public class Tunnel extends BaseObservable implements Comparable<Tunnel> { + public static final int NAME_MAX_LENGTH = 16; + private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,16}"); + private static final String TAG = Tunnel.class.getSimpleName(); + + private final Backend backend; + private final ConfigStore configStore; + private final String name; + private Config config; + private Instant lastStateChange = Instant.EPOCH; + private State state = State.UNKNOWN; + private Statistics statistics; + + Tunnel(@NonNull final Backend backend, @NonNull final ConfigStore configStore, + @NonNull final String name, @Nullable final Config config) { + this.backend = backend; + this.configStore = configStore; + this.name = name; + this.config = config; + } + + public static boolean isNameValid(final CharSequence name) { + return name != null && NAME_PATTERN.matcher(name).matches(); + } + + @Override + public int compareTo(@NonNull final Tunnel tunnel) { + return name.compareTo(tunnel.name); + } + + @Bindable + public Config getConfig() { + if (config == null) + getConfigAsync().whenComplete(ExceptionLoggers.D); + return config; + } + + public CompletionStage<Config> getConfigAsync() { + if (config == null) + return configStore.load(name).thenApply(this::setConfigInternal); + return CompletableFuture.completedFuture(config); + } + + @Bindable + public Instant getLastStateChange() { + return lastStateChange; + } + + @Bindable + public String getName() { + return name; + } + + @Bindable + public State getState() { + if (state == State.UNKNOWN) + getStateAsync().whenComplete(ExceptionLoggers.D); + return state; + } + + public CompletionStage<State> getStateAsync() { + if (state == State.UNKNOWN) + return backend.getState(this).thenApply(this::setStateInternal); + return CompletableFuture.completedFuture(state); + } + + @Bindable + public Statistics getStatistics() { + // FIXME: Check age of statistics. + if (statistics == null) + getStatisticsAsync().whenComplete(ExceptionLoggers.D); + return statistics; + } + + public CompletionStage<Statistics> getStatisticsAsync() { + // FIXME: Check age of statistics. + if (statistics == null) + return backend.getStatistics(this).thenApply(this::setStatisticsInternal); + return CompletableFuture.completedFuture(statistics); + } + + private void onStateChanged(final State oldState, final State newState) { + if (oldState != State.UNKNOWN) { + lastStateChange = Instant.now(); + notifyPropertyChanged(BR.lastStateChange); + } + if (newState != State.UP) + setStatisticsInternal(null); + } + + public CompletionStage<Config> setConfig(@NonNull final Config config) { + if (!config.equals(this.config)) { + return backend.applyConfig(this, config) + .thenCompose(cfg -> configStore.save(name, cfg)) + .thenApply(this::setConfigInternal); + } + return CompletableFuture.completedFuture(this.config); + } + + private Config setConfigInternal(final Config config) { + if (Objects.equals(this.config, config)) + return config; + this.config = config; + notifyPropertyChanged(BR.config); + return config; + } + + public CompletionStage<State> setState(@NonNull final State state) { + if (state != this.state) + return backend.setState(this, state) + .thenApply(this::setStateInternal); + return CompletableFuture.completedFuture(this.state); + } + + private State setStateInternal(final State state) { + if (Objects.equals(this.state, state)) + return state; + onStateChanged(this.state, state); + this.state = state; + notifyPropertyChanged(BR.state); + return state; + } + + private Statistics setStatisticsInternal(final Statistics statistics) { + if (Objects.equals(this.statistics, statistics)) + return statistics; + this.statistics = statistics; + notifyPropertyChanged(BR.statistics); + return statistics; + } + + public enum State { + DOWN, + TOGGLE, + UNKNOWN, + UP + } + + public static class Statistics extends BaseObservable { + } +} diff --git a/app/src/main/java/com/wireguard/android/model/TunnelCollection.java b/app/src/main/java/com/wireguard/android/model/TunnelCollection.java new file mode 100644 index 00000000..38b5165a --- /dev/null +++ b/app/src/main/java/com/wireguard/android/model/TunnelCollection.java @@ -0,0 +1,10 @@ +package com.wireguard.android.model; + +import com.wireguard.android.databinding.ObservableTreeMap; + +/** + * Created by samuel on 12/19/17. + */ + +public class TunnelCollection extends ObservableTreeMap<String, Tunnel> { +} diff --git a/app/src/main/java/com/wireguard/android/model/TunnelManager.java b/app/src/main/java/com/wireguard/android/model/TunnelManager.java new file mode 100644 index 00000000..5122f9bf --- /dev/null +++ b/app/src/main/java/com/wireguard/android/model/TunnelManager.java @@ -0,0 +1,111 @@ +package com.wireguard.android.model; + +import android.content.SharedPreferences; +import android.util.Log; + +import com.wireguard.android.Application.ApplicationScope; +import com.wireguard.android.backend.Backend; +import com.wireguard.android.configStore.ConfigStore; +import com.wireguard.android.model.Tunnel.State; +import com.wireguard.android.util.ExceptionLoggers; +import com.wireguard.config.Config; + +import java.util.Collections; +import java.util.Set; + +import javax.inject.Inject; + +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, + */ + +@ApplicationScope +public final class TunnelManager { + public static final String KEY_PRIMARY_TUNNEL = "primary_config"; + public static final String KEY_SELECTED_TUNNEL = "selected_tunnel"; + private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot"; + private static final String KEY_RUNNING_TUNNELS = "enabled_configs"; + private static final String TAG = TunnelManager.class.getSimpleName(); + + private final Backend backend; + private final ConfigStore configStore; + private final SharedPreferences preferences; + private final TunnelCollection tunnels = new TunnelCollection(); + + @Inject + public TunnelManager(final Backend backend, final ConfigStore configStore, + final SharedPreferences preferences) { + this.backend = backend; + this.configStore = configStore; + this.preferences = preferences; + } + + private Tunnel add(final String name, final Config config) { + final Tunnel tunnel = new Tunnel(backend, configStore, name, config); + tunnels.put(name, tunnel); + return tunnel; + } + + private Tunnel add(final String name) { + return add(name, null); + } + + public CompletionStage<Tunnel> create(final String name, final Config config) { + Log.v(TAG, "Requested create tunnel " + name + " with config\n" + config); + if (!Tunnel.isNameValid(name)) + return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name")); + if (tunnels.containsKey(name)) { + final String message = "Tunnel " + name + " already exists"; + return CompletableFuture.failedFuture(new IllegalArgumentException(message)); + } + return configStore.create(name, config).thenApply(savedConfig -> add(name, savedConfig)); + } + + public CompletionStage<Void> delete(final Tunnel tunnel) { + Log.v(TAG, "Requested delete tunnel " + tunnel.getName() + " state=" + tunnel.getState()); + return backend.setState(tunnel, State.DOWN) + .thenCompose(x -> configStore.delete(tunnel.getName())) + .thenAccept(x -> tunnels.remove(tunnel.getName())); + } + + public TunnelCollection getTunnels() { + return tunnels; + } + + public void onCreate() { + Log.v(TAG, "onCreate triggered"); + configStore.enumerate() + .thenApply(names -> StreamSupport.stream(names) + .map(this::add) + .map(Tunnel::getStateAsync) + .toArray(CompletableFuture[]::new)) + .thenCompose(CompletableFuture::allOf) + .whenComplete(ExceptionLoggers.E); + } + + public CompletionStage<Void> restoreState() { + if (!preferences.getBoolean(KEY_RESTORE_ON_BOOT, false)) + return CompletableFuture.completedFuture(null); + final Set<String> tunnelsToEnable = + preferences.getStringSet(KEY_RUNNING_TUNNELS, Collections.emptySet()); + final CompletableFuture[] futures = StreamSupport.stream(tunnelsToEnable) + .map(tunnels::get) + .map(tunnel -> tunnel.setState(State.UP)) + .toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(futures); + } + + public CompletionStage<Void> saveState() { + final Set<String> runningTunnels = StreamSupport.stream(tunnels.values()) + .filter(tunnel -> tunnel.getState() == State.UP) + .map(Tunnel::getName) + .collect(Collectors.toUnmodifiableSet()); + preferences.edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply(); + return CompletableFuture.completedFuture(null); + } +} |