TunnelManager/QuickTileService: Remember last used tunnel

This is actually a bit simpler than having a manually-selected "primary"
tunnel, and is hopefully easier for the user.

Signed-off-by: Samuel Holland <samuel@sholland.org>
This commit is contained in:
Samuel Holland 2018-01-07 21:50:43 -06:00
parent 38b2aafce8
commit 1fd9547f6a
2 changed files with 110 additions and 101 deletions

View File

@ -2,12 +2,8 @@ package com.wireguard.android;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.databinding.Observable; import android.databinding.Observable;
import android.databinding.Observable.OnPropertyChangedCallback; import android.databinding.Observable.OnPropertyChangedCallback;
import android.databinding.ObservableList;
import android.databinding.ObservableList.OnListChangedCallback;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
import android.os.Build; import android.os.Build;
import android.service.quicksettings.Tile; import android.service.quicksettings.Tile;
@ -15,13 +11,10 @@ import android.service.quicksettings.TileService;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import com.wireguard.android.Application.ApplicationComponent;
import com.wireguard.android.activity.MainActivity; import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.activity.SettingsActivity;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.TunnelManager; import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.ObservableKeyedList;
import java.util.Objects; import java.util.Objects;
@ -32,61 +25,40 @@ import java.util.Objects;
*/ */
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)
public class QuickTileService extends TileService implements OnSharedPreferenceChangeListener { public class QuickTileService extends TileService {
private static final String TAG = QuickTileService.class.getSimpleName(); private static final String TAG = QuickTileService.class.getSimpleName();
private final OnTunnelListChangedCallback listCallback = new OnTunnelListChangedCallback(); private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
private final OnTunnelStateChangedCallback tunnelCallback = new OnTunnelStateChangedCallback(); private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
private SharedPreferences preferences;
private Tunnel tunnel; private Tunnel tunnel;
private TunnelManager tunnelManager; private TunnelManager tunnelManager;
@Override @Override
public void onClick() { public void onClick() {
if (tunnel != null) { if (tunnel != null)
tunnel.setState(State.TOGGLE).handle(this::onToggleFinished); tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished);
} else { else
if (tunnelManager.getTunnels().isEmpty()) { startActivityAndCollapse(new Intent(this, MainActivity.class));
// Prompt the user to create or import a tunnel configuration.
startActivityAndCollapse(new Intent(this, MainActivity.class));
} else {
// Prompt the user to select a tunnel for use with the quick settings tile.
final Intent intent = new Intent(this, SettingsActivity.class);
intent.putExtra(SettingsActivity.KEY_SHOW_QUICK_TILE_SETTINGS, true);
startActivityAndCollapse(intent);
}
}
} }
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
final ApplicationComponent component = Application.getComponent(); tunnelManager = Application.getComponent().getTunnelManager();
preferences = component.getPreferences();
tunnelManager = component.getTunnelManager();
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences preferences, final String key) {
if (!TunnelManager.KEY_PRIMARY_TUNNEL.equals(key))
return;
updateTile();
} }
@Override @Override
public void onStartListening() { public void onStartListening() {
preferences.registerOnSharedPreferenceChangeListener(this); tunnelManager.addOnPropertyChangedCallback(onTunnelChangedCallback);
tunnelManager.getTunnels().addOnListChangedCallback(listCallback);
if (tunnel != null) if (tunnel != null)
tunnel.addOnPropertyChangedCallback(tunnelCallback); tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
updateTile(); updateTile();
} }
@Override @Override
public void onStopListening() { public void onStopListening() {
preferences.unregisterOnSharedPreferenceChangeListener(this);
tunnelManager.getTunnels().removeOnListChangedCallback(listCallback);
if (tunnel != null) if (tunnel != null)
tunnel.removeOnPropertyChangedCallback(tunnelCallback); tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
tunnelManager.removeOnPropertyChangedCallback(onTunnelChangedCallback);
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -101,16 +73,13 @@ public class QuickTileService extends TileService implements OnSharedPreferenceC
private void updateTile() { private void updateTile() {
// Update the tunnel. // Update the tunnel.
final String currentName = tunnel != null ? tunnel.getName() : null; final Tunnel newTunnel = tunnelManager.getLastUsedTunnel();
final String newName = preferences.getString(TunnelManager.KEY_PRIMARY_TUNNEL, null); if (newTunnel != tunnel) {
if (!Objects.equals(currentName, newName)) {
final ObservableKeyedList<String, Tunnel> tunnels = tunnelManager.getTunnels();
final Tunnel newTunnel = newName != null ? tunnels.get(newName) : null;
if (tunnel != null) if (tunnel != null)
tunnel.removeOnPropertyChangedCallback(tunnelCallback); tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
tunnel = newTunnel; tunnel = newTunnel;
if (tunnel != null) if (tunnel != null)
tunnel.addOnPropertyChangedCallback(tunnelCallback); tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
} }
// Update the tile contents. // Update the tile contents.
final String label; final String label;
@ -126,48 +95,15 @@ public class QuickTileService extends TileService implements OnSharedPreferenceC
tile.setLabel(label); tile.setLabel(label);
if (tile.getState() != state) { if (tile.getState() != state) {
// The icon must be changed every time the state changes, or the shade will not change. // The icon must be changed every time the state changes, or the shade will not change.
final Integer iconResource = (state == Tile.STATE_ACTIVE) final Integer iconResource = state == Tile.STATE_ACTIVE ? R.drawable.ic_tile
? R.drawable.ic_tile : R.drawable.ic_tile_disabled; : R.drawable.ic_tile_disabled;
tile.setIcon(Icon.createWithResource(this, iconResource)); tile.setIcon(Icon.createWithResource(this, iconResource));
tile.setState(state); tile.setState(state);
} }
tile.updateTile(); tile.updateTile();
} }
private final class OnTunnelListChangedCallback private final class OnStateChangedCallback extends OnPropertyChangedCallback {
extends OnListChangedCallback<ObservableList<Tunnel>> {
@Override
public void onChanged(final ObservableList<Tunnel> sender) {
updateTile();
}
@Override
public void onItemRangeChanged(final ObservableList<Tunnel> sender,
final int positionStart, final int itemCount) {
updateTile();
}
@Override
public void onItemRangeInserted(final ObservableList<Tunnel> sender,
final int positionStart, final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeMoved(final ObservableList<Tunnel> sender,
final int fromPosition, final int toPosition,
final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeRemoved(final ObservableList<Tunnel> sender,
final int positionStart, final int itemCount) {
updateTile();
}
}
private final class OnTunnelStateChangedCallback extends OnPropertyChangedCallback {
@Override @Override
public void onPropertyChanged(final Observable sender, final int propertyId) { public void onPropertyChanged(final Observable sender, final int propertyId) {
if (!Objects.equals(sender, tunnel)) { if (!Objects.equals(sender, tunnel)) {
@ -179,4 +115,13 @@ public class QuickTileService extends TileService implements OnSharedPreferenceC
updateTile(); updateTile();
} }
} }
private final class OnTunnelChangedCallback extends OnPropertyChangedCallback {
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
return;
updateTile();
}
}
} }

View File

@ -1,9 +1,12 @@
package com.wireguard.android.model; package com.wireguard.android.model;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.wireguard.android.Application.ApplicationScope; import com.wireguard.android.Application.ApplicationScope;
import com.wireguard.android.BR;
import com.wireguard.android.backend.Backend; import com.wireguard.android.backend.Backend;
import com.wireguard.android.configStore.ConfigStore; import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.Tunnel.State;
@ -14,7 +17,6 @@ import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.android.util.ObservableSortedKeyedArrayList; import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Set; import java.util.Set;
@ -31,10 +33,10 @@ import java9.util.stream.StreamSupport;
*/ */
@ApplicationScope @ApplicationScope
public final class TunnelManager { public final class TunnelManager extends BaseObservable {
public static final String KEY_PRIMARY_TUNNEL = "primary_config";
private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing( private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder()); 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_RESTORE_ON_BOOT = "restore_on_boot";
private static final String KEY_RUNNING_TUNNELS = "enabled_configs"; private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
private static final String TAG = TunnelManager.class.getSimpleName(); private static final String TAG = TunnelManager.class.getSimpleName();
@ -45,6 +47,7 @@ public final class TunnelManager {
private final SharedPreferences preferences; private final SharedPreferences preferences;
private final ObservableKeyedList<String, Tunnel> tunnels = private final ObservableKeyedList<String, Tunnel> tunnels =
new ObservableSortedKeyedArrayList<>(COMPARATOR); new ObservableSortedKeyedArrayList<>(COMPARATOR);
private Tunnel lastUsedTunnel;
@Inject @Inject
public TunnelManager(final AsyncWorker asyncWorker, final Backend backend, public TunnelManager(final AsyncWorker asyncWorker, final Backend backend,
@ -73,16 +76,38 @@ public final class TunnelManager {
} }
CompletionStage<Void> delete(final Tunnel tunnel) { CompletionStage<Void> delete(final Tunnel 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 asyncWorker.runAsync(() -> { return asyncWorker.runAsync(() -> {
backend.setState(tunnel, State.DOWN); if (originalState == State.UP)
configStore.delete(tunnel.getName()); backend.setState(tunnel, State.DOWN);
}).thenAccept(x -> { try {
if (tunnel.getName().equals(preferences.getString(KEY_PRIMARY_TUNNEL, null))) configStore.delete(tunnel.getName());
preferences.edit().remove(KEY_PRIMARY_TUNNEL).apply(); } catch (final Exception e) {
tunnels.remove(tunnel); if (originalState == State.UP)
backend.setState(tunnel, originalState);
// 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
public Tunnel getLastUsedTunnel() {
return lastUsedTunnel;
}
CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) { CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) {
final CompletionStage<Config> completion = final CompletionStage<Config> completion =
asyncWorker.supplyAsync(() -> configStore.load(tunnel.getName())); asyncWorker.supplyAsync(() -> configStore.load(tunnel.getName()));
@ -117,6 +142,9 @@ public final class TunnelManager {
private void onTunnelsLoaded(final Set<String> present, final Set<String> running) { private void onTunnelsLoaded(final Set<String> present, final Set<String> running) {
for (final String name : present) for (final String name : present)
addToList(name, null, running.contains(name) ? State.UP : State.DOWN); addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
final String lastUsedName = preferences.getString(KEY_LAST_USED_TUNNEL, null);
if (lastUsedName != null)
setLastUsedTunnel(tunnels.get(lastUsedName));
} }
CompletionStage<Tunnel> rename(final Tunnel tunnel, final String name) { CompletionStage<Tunnel> rename(final Tunnel tunnel, final String name) {
@ -127,21 +155,42 @@ public final class TunnelManager {
return CompletableFuture.failedFuture(new IllegalArgumentException(message)); return CompletableFuture.failedFuture(new IllegalArgumentException(message));
} }
final State originalState = tunnel.getState(); final State originalState = tunnel.getState();
final boolean wasLastUsed = tunnel == lastUsedTunnel;
// Make sure nothing touches the tunnel.
if (wasLastUsed)
setLastUsedTunnel(null);
tunnels.remove(tunnel);
return asyncWorker.supplyAsync(() -> { return asyncWorker.supplyAsync(() -> {
backend.setState(tunnel, State.DOWN); if (originalState == State.UP)
backend.setState(tunnel, State.DOWN);
final Config newConfig = configStore.create(name, tunnel.getConfig()); final Config newConfig = configStore.create(name, tunnel.getConfig());
final Tunnel newTunnel = new Tunnel(this, name, newConfig, State.DOWN); final Tunnel newTunnel = new Tunnel(this, name, newConfig, State.DOWN);
if (originalState == State.UP) { try {
backend.setState(newTunnel, originalState); if (originalState == State.UP)
newTunnel.onStateChanged(originalState); backend.setState(newTunnel, originalState);
configStore.delete(tunnel.getName());
} catch (final Exception e) {
// Clean up.
configStore.delete(name);
if (originalState == State.UP)
backend.setState(tunnel, originalState);
// Re-throw the exception to fail the completion.
throw e;
} }
configStore.delete(tunnel.getName());
return newTunnel; return newTunnel;
}).whenComplete((newTunnel, e) -> { }).whenComplete((newTunnel, e) -> {
if (e != null) if (e == null) {
return; // Success, add the new tunnel.
tunnels.remove(tunnel); newTunnel.onStateChanged(originalState);
tunnels.add(newTunnel); tunnels.add(newTunnel);
if (wasLastUsed)
setLastUsedTunnel(newTunnel);
} else {
// Failure, put the old tunnel back.
tunnels.add(tunnel);
if (wasLastUsed)
setLastUsedTunnel(tunnel);
}
}); });
} }
@ -166,6 +215,17 @@ public final class TunnelManager {
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
} }
private void setLastUsedTunnel(final Tunnel tunnel) {
if (tunnel == lastUsedTunnel)
return;
lastUsedTunnel = tunnel;
notifyPropertyChanged(BR.lastUsedTunnel);
if (tunnel != null)
preferences.edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).apply();
else
preferences.edit().remove(KEY_LAST_USED_TUNNEL).apply();
}
CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) { CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) {
final CompletionStage<Config> completion = asyncWorker.supplyAsync(() -> { final CompletionStage<Config> completion = asyncWorker.supplyAsync(() -> {
final Config appliedConfig = backend.applyConfig(tunnel, config); final Config appliedConfig = backend.applyConfig(tunnel, config);
@ -179,6 +239,10 @@ public final class TunnelManager {
final CompletionStage<State> completion = final CompletionStage<State> completion =
asyncWorker.supplyAsync(() -> backend.setState(tunnel, state)); asyncWorker.supplyAsync(() -> backend.setState(tunnel, state));
completion.thenAccept(tunnel::onStateChanged); completion.thenAccept(tunnel::onStateChanged);
completion.thenAccept(newState -> {
if (newState == State.UP)
setLastUsedTunnel(tunnel);
});
return completion; return completion;
} }
} }