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>master
@@ -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<String, Tunnel> 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<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 { | |||
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(); | |||
} | |||
} | |||
} |
@@ -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<String> COMPARATOR = Comparators.<String>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<String, Tunnel> 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<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(() -> { | |||
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<Config> getTunnelConfig(final Tunnel tunnel) { | |||
final CompletionStage<Config> completion = | |||
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) { | |||
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<Tunnel> 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<Config> setTunnelConfig(final Tunnel tunnel, final Config config) { | |||
final CompletionStage<Config> completion = asyncWorker.supplyAsync(() -> { | |||
final Config appliedConfig = backend.applyConfig(tunnel, config); | |||
@@ -179,6 +239,10 @@ public final class TunnelManager { | |||
final CompletionStage<State> completion = | |||
asyncWorker.supplyAsync(() -> backend.setState(tunnel, state)); | |||
completion.thenAccept(tunnel::onStateChanged); | |||
completion.thenAccept(newState -> { | |||
if (newState == State.UP) | |||
setLastUsedTunnel(tunnel); | |||
}); | |||
return completion; | |||
} | |||
} |