Serviceless rewrite, part 1
Signed-off-by: Samuel Holland <samuel@sholland.org>
This commit is contained in:
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user