From 1fd9547f6a57edb45c517b17baf62fa66af20cb6 Mon Sep 17 00:00:00 2001 From: Samuel Holland Date: Sun, 7 Jan 2018 21:50:43 -0600 Subject: [PATCH] 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 --- .../wireguard/android/QuickTileService.java | 111 +++++------------- .../android/model/TunnelManager.java | 100 +++++++++++++--- 2 files changed, 110 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/com/wireguard/android/QuickTileService.java b/app/src/main/java/com/wireguard/android/QuickTileService.java index 3b652a5..a766d6c 100644 --- a/app/src/main/java/com/wireguard/android/QuickTileService.java +++ b/app/src/main/java/com/wireguard/android/QuickTileService.java @@ -2,12 +2,8 @@ package com.wireguard.android; import android.annotation.TargetApi; import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.databinding.Observable; import android.databinding.Observable.OnPropertyChangedCallback; -import android.databinding.ObservableList; -import android.databinding.ObservableList.OnListChangedCallback; import android.graphics.drawable.Icon; import android.os.Build; import android.service.quicksettings.Tile; @@ -15,13 +11,10 @@ import android.service.quicksettings.TileService; import android.util.Log; import android.widget.Toast; -import com.wireguard.android.Application.ApplicationComponent; import com.wireguard.android.activity.MainActivity; -import com.wireguard.android.activity.SettingsActivity; import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.TunnelManager; -import com.wireguard.android.util.ObservableKeyedList; import java.util.Objects; @@ -32,61 +25,40 @@ import java.util.Objects; */ @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 final OnTunnelListChangedCallback listCallback = new OnTunnelListChangedCallback(); - private final OnTunnelStateChangedCallback tunnelCallback = new OnTunnelStateChangedCallback(); - private SharedPreferences preferences; + private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback(); + private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback(); private Tunnel tunnel; private TunnelManager tunnelManager; @Override public void onClick() { - if (tunnel != null) { - tunnel.setState(State.TOGGLE).handle(this::onToggleFinished); - } else { - if (tunnelManager.getTunnels().isEmpty()) { - // 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); - } - } + if (tunnel != null) + tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished); + else + startActivityAndCollapse(new Intent(this, MainActivity.class)); } @Override public void onCreate() { super.onCreate(); - final ApplicationComponent component = Application.getComponent(); - 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(); + tunnelManager = Application.getComponent().getTunnelManager(); } @Override public void onStartListening() { - preferences.registerOnSharedPreferenceChangeListener(this); - tunnelManager.getTunnels().addOnListChangedCallback(listCallback); + tunnelManager.addOnPropertyChangedCallback(onTunnelChangedCallback); if (tunnel != null) - tunnel.addOnPropertyChangedCallback(tunnelCallback); + tunnel.addOnPropertyChangedCallback(onStateChangedCallback); updateTile(); } @Override public void onStopListening() { - preferences.unregisterOnSharedPreferenceChangeListener(this); - tunnelManager.getTunnels().removeOnListChangedCallback(listCallback); if (tunnel != null) - tunnel.removeOnPropertyChangedCallback(tunnelCallback); + tunnel.removeOnPropertyChangedCallback(onStateChangedCallback); + tunnelManager.removeOnPropertyChangedCallback(onTunnelChangedCallback); } @SuppressWarnings("unused") @@ -101,16 +73,13 @@ public class QuickTileService extends TileService implements OnSharedPreferenceC private void updateTile() { // Update the tunnel. - final String currentName = tunnel != null ? tunnel.getName() : null; - final String newName = preferences.getString(TunnelManager.KEY_PRIMARY_TUNNEL, null); - if (!Objects.equals(currentName, newName)) { - final ObservableKeyedList tunnels = tunnelManager.getTunnels(); - final Tunnel newTunnel = newName != null ? tunnels.get(newName) : null; + final Tunnel newTunnel = tunnelManager.getLastUsedTunnel(); + if (newTunnel != tunnel) { if (tunnel != null) - tunnel.removeOnPropertyChangedCallback(tunnelCallback); + tunnel.removeOnPropertyChangedCallback(onStateChangedCallback); tunnel = newTunnel; if (tunnel != null) - tunnel.addOnPropertyChangedCallback(tunnelCallback); + tunnel.addOnPropertyChangedCallback(onStateChangedCallback); } // Update the tile contents. final String label; @@ -126,48 +95,15 @@ public class QuickTileService extends TileService implements OnSharedPreferenceC tile.setLabel(label); if (tile.getState() != state) { // The icon must be changed every time the state changes, or the shade will not change. - final Integer iconResource = (state == Tile.STATE_ACTIVE) - ? R.drawable.ic_tile : R.drawable.ic_tile_disabled; + final Integer iconResource = state == Tile.STATE_ACTIVE ? R.drawable.ic_tile + : R.drawable.ic_tile_disabled; tile.setIcon(Icon.createWithResource(this, iconResource)); tile.setState(state); } tile.updateTile(); } - private final class OnTunnelListChangedCallback - extends OnListChangedCallback> { - @Override - public void onChanged(final ObservableList sender) { - updateTile(); - } - - @Override - public void onItemRangeChanged(final ObservableList sender, - final int positionStart, final int itemCount) { - updateTile(); - } - - @Override - public void onItemRangeInserted(final ObservableList sender, - final int positionStart, final int itemCount) { - // Do nothing. - } - - @Override - public void onItemRangeMoved(final ObservableList sender, - final int fromPosition, final int toPosition, - final int itemCount) { - // Do nothing. - } - - @Override - public void onItemRangeRemoved(final ObservableList sender, - final int positionStart, final int itemCount) { - updateTile(); - } - } - - private final class OnTunnelStateChangedCallback extends OnPropertyChangedCallback { + private final class OnStateChangedCallback extends OnPropertyChangedCallback { @Override public void onPropertyChanged(final Observable sender, final int propertyId) { if (!Objects.equals(sender, tunnel)) { @@ -179,4 +115,13 @@ public class QuickTileService extends TileService implements OnSharedPreferenceC 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(); + } + } } diff --git a/app/src/main/java/com/wireguard/android/model/TunnelManager.java b/app/src/main/java/com/wireguard/android/model/TunnelManager.java index 8b7ef20..09c33b2 100644 --- a/app/src/main/java/com/wireguard/android/model/TunnelManager.java +++ b/app/src/main/java/com/wireguard/android/model/TunnelManager.java @@ -1,9 +1,12 @@ package com.wireguard.android.model; import android.content.SharedPreferences; +import android.databinding.BaseObservable; +import android.databinding.Bindable; import android.support.annotation.NonNull; import com.wireguard.android.Application.ApplicationScope; +import com.wireguard.android.BR; import com.wireguard.android.backend.Backend; import com.wireguard.android.configStore.ConfigStore; 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.config.Config; -import java.util.Collections; import java.util.Comparator; import java.util.Set; @@ -31,10 +33,10 @@ import java9.util.stream.StreamSupport; */ @ApplicationScope -public final class TunnelManager { - public static final String KEY_PRIMARY_TUNNEL = "primary_config"; +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 static final String TAG = TunnelManager.class.getSimpleName(); @@ -45,6 +47,7 @@ public final class TunnelManager { private final SharedPreferences preferences; private final ObservableKeyedList tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR); + private Tunnel lastUsedTunnel; @Inject public TunnelManager(final AsyncWorker asyncWorker, final Backend backend, @@ -73,16 +76,38 @@ public final class TunnelManager { } CompletionStage 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(() -> { - backend.setState(tunnel, State.DOWN); - configStore.delete(tunnel.getName()); - }).thenAccept(x -> { - if (tunnel.getName().equals(preferences.getString(KEY_PRIMARY_TUNNEL, null))) - preferences.edit().remove(KEY_PRIMARY_TUNNEL).apply(); - tunnels.remove(tunnel); + if (originalState == State.UP) + backend.setState(tunnel, State.DOWN); + try { + configStore.delete(tunnel.getName()); + } catch (final Exception e) { + 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 getTunnelConfig(final Tunnel tunnel) { final CompletionStage completion = asyncWorker.supplyAsync(() -> configStore.load(tunnel.getName())); @@ -117,6 +142,9 @@ public final class TunnelManager { private void onTunnelsLoaded(final Set present, final Set running) { for (final String name : present) 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 rename(final Tunnel tunnel, final String name) { @@ -127,21 +155,42 @@ public final class TunnelManager { 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 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 Tunnel newTunnel = new Tunnel(this, name, newConfig, State.DOWN); - if (originalState == State.UP) { - backend.setState(newTunnel, originalState); - newTunnel.onStateChanged(originalState); + try { + if (originalState == State.UP) + 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; }).whenComplete((newTunnel, e) -> { - if (e != null) - return; - tunnels.remove(tunnel); - tunnels.add(newTunnel); + if (e == null) { + // Success, add the new tunnel. + newTunnel.onStateChanged(originalState); + 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); } + 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 setTunnelConfig(final Tunnel tunnel, final Config config) { final CompletionStage completion = asyncWorker.supplyAsync(() -> { final Config appliedConfig = backend.applyConfig(tunnel, config); @@ -179,6 +239,10 @@ public final class TunnelManager { final CompletionStage completion = asyncWorker.supplyAsync(() -> backend.setState(tunnel, state)); completion.thenAccept(tunnel::onStateChanged); + completion.thenAccept(newState -> { + if (newState == State.UP) + setLastUsedTunnel(tunnel); + }); return completion; } }