Compare commits

..

22 Commits

Author SHA1 Message Date
Jason A. Donenfeld 3cb3e9d8b7 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:52:54 +02:00
Jason A. Donenfeld 5dfc5659ad ConfigNamingDialog: use name input filter
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:44:30 +02:00
Jason A. Donenfeld c4102992ae MonkeyedSnackbar: work around Harsh's broken phone
I think I'd probably like to revert this, since presumably there's a
good reason in the first place why the support lib disables animations
when accessibility services are turned on?

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:35:26 +02:00
Jason A. Donenfeld 219f4e8016 MSF: make it more obvious what needs to be done
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 17:21:30 +02:00
Jason A. Donenfeld 6558140a7c FloatingActionButtonBehavior: animate transitions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 17:21:30 +02:00
Jason A. Donenfeld db7b61ab80 Roll back to API 27 for now
There's no source available for API 28, which is a pain. But this commit
should be reverted whenever source is released.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 16:01:44 +02:00
Jason A. Donenfeld bce5d852e1 Target API 28
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 15:46:01 +02:00
Jason A. Donenfeld b960b4a6cd TunnelListFragment: show selected tunnel
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 05:57:53 +02:00
Jason A. Donenfeld 1b10e75168 TunnelListFragment: fix multiselection on rotation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 04:50:16 +02:00
Jason A. Donenfeld 07359e392c FloatingActionButonBehavior: set translation back to 0 when snackbar dies
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 04:23:30 +02:00
Jason A. Donenfeld e5a5bad240 MainActivity: only show two column on tablets
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 03:46:45 +02:00
Samuel Holland ca92ac60b7 MainActivity: Fix fragment selection logic
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-07-28 17:07:37 -05:00
Jason A. Donenfeld e29c21f8df Application: refuse to run on old android
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-28 18:27:45 +02:00
Jason A. Donenfeld 6ceeac93bf QuickTileService: fix bug the wrong way
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-28 17:55:24 +02:00
Jason A. Donenfeld 9f861096ac config: show more informative error message on wrong key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-27 15:20:23 +02:00
Jason A. Donenfeld 520df16885 Set ACRA install source
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 13:46:18 +02:00
Jason A. Donenfeld c905ef6083 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 05:29:49 +02:00
Eric Kuck 9652fe99df TunnelDetailFragment now restores state correctly after process death
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 05:17:43 +02:00
Jason A. Donenfeld 62d8beff96 Application: use proper completablefuture for backend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 04:54:14 +02:00
Jason A. Donenfeld 7d438e9dbc Wire up ACRA
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 01:41:29 +02:00
Harsh Shandilya b364221c93 FloatingActionBehaviour: Adjust constructors
Get these in line with the parent class

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-26 01:36:53 +02:00
Jason A. Donenfeld b8052bd8fb ThemeChangeAwareActivity: reintroduce cache buster
This is still needed by certain icons, like the trash icon in the peer
editor.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 04:05:41 +02:00
29 changed files with 502 additions and 223 deletions
+6 -2
View File
@@ -17,8 +17,9 @@ android {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 27
versionCode 436
versionName '0.0.20180724'
versionCode 438
versionName '0.0.20180729'
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
}
// If the keystore file exists
if (keystorePropertiesFile.exists()) {
@@ -69,6 +70,7 @@ ext {
streamsupportVersion = '1.6.0'
jsr305Version = '3.0.2'
zxingEmbeddedVersion = '3.6.0'
acraVersion = '5.2.0-rc2'
}
dependencies {
@@ -82,6 +84,8 @@ dependencies {
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "ch.acra:acra-http:$acraVersion"
}
tasks.withType(JavaCompile) {
@@ -5,8 +5,13 @@
package com.wireguard.android;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
@@ -16,20 +21,36 @@ import android.support.v7.app.AppCompatDelegate;
import com.wireguard.android.backend.Backend;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.configStore.FileConfigStore;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.AsyncWorker;
import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraHttpSender;
import org.acra.data.StringFormat;
import org.acra.sender.HttpSender;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java9.util.concurrent.CompletableFuture;
@AcraCore(reportFormat = StringFormat.JSON,
buildConfigClass = BuildConfig.class,
logcatArguments = {"-b", "all", "-d", "-v", "threadtime", "*:V"},
excludeMatchingSharedPreferencesKeys = {"last_used_tunnel", "enabled_configs"})
@AcraHttpSender(uri = "https://crashreport.zx2c4.com/android/report",
basicAuthLogin = "6RCovLxEVCTXGiW5",
basicAuthPassword = "O7I3sVa5ULVdiC51",
httpMethod = HttpSender.Method.POST,
compress = true)
public class Application extends android.app.Application {
@SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf;
@SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker;
@@ -37,15 +58,65 @@ public class Application extends android.app.Application {
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
@SuppressWarnings("NullableProblems") private TunnelManager tunnelManager;
@SuppressWarnings("NullableProblems") private Handler handler;
@Nullable private Backend backend;
@Nullable private Collection<BackendCallback> haveBackendCallbacks = new ArrayList<>();
private final Object haveBackendCallbacksLock = new Object();
private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
public Application() {
weakSelf = new WeakReference<>(this);
}
/* The ACRA password can be trivially reverse engineered and is open source anyway,
* so there's no point in trying to protect it. However, we do want to at least
* prevent innocent self-builders from uploading stuff to our crash reporter. So, we
* check the DN of the certs that signed the apk, without even bothering to try
* validating that they're authentic. It's a good enough heuristic.
*/
@SuppressWarnings("deprecation")
@Nullable
private static String getInstallSource(final Context context) {
if (BuildConfig.DEBUG)
return null;
try {
final CertificateFactory cf = CertificateFactory.getInstance("X509");
for (final Signature sig : context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures) {
try {
for (final String category : ((X509Certificate) cf.generateCertificate(new ByteArrayInputStream(sig.toByteArray()))).getSubjectDN().getName().split(", *")) {
final String[] parts = category.split("=", 2);
if (!"O".equals(parts[0]))
continue;
switch (parts[1]) {
case "Google Inc.":
return "Play Store";
case "fdroid.org":
return "F-Droid";
}
}
} catch (final Exception ignored) { }
}
} catch (final Exception ignored) { }
return null;
}
@Override
protected void attachBaseContext(final Context context) {
super.attachBaseContext(context);
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
System.exit(0);
}
final String installSource = getInstallSource(context);
if (installSource != null) {
ACRA.init(this);
ACRA.getErrorReporter().putCustomData("installSource", installSource);
}
}
public static Application get() {
return weakSelf.get();
}
@@ -56,43 +127,26 @@ public class Application extends android.app.Application {
public static Backend getBackend() {
final Application app = get();
synchronized (app) {
synchronized (app.futureBackend) {
if (app.backend == null) {
Backend backend = null;
if (new File("/sys/module/wireguard").exists()) {
try {
app.rootShell.start();
app.backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) { }
}
if (app.backend == null)
app.backend = new GoBackend(app.getApplicationContext());
synchronized (app.haveBackendCallbacksLock) {
if (app.haveBackendCallbacks != null) {
for (final BackendCallback callback : app.haveBackendCallbacks)
app.handler.post(() -> callback.callback(app.backend));
app.haveBackendCallbacks = null;
backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) {
}
}
if (backend == null)
backend = new GoBackend(app.getApplicationContext());
app.backend = backend;
}
return app.backend;
}
}
@FunctionalInterface
public interface BackendCallback {
void callback(final Backend backend);
}
public static void onHaveBackend(final BackendCallback callback) {
final Application app = get();
synchronized (app.haveBackendCallbacksLock) {
if (app.haveBackendCallbacks == null) {
Objects.requireNonNull(app.backend, "Backend still null in onHaveBackend call");
callback.callback(app.backend);
} else {
app.haveBackendCallbacks.add(callback);
}
}
public static CompletableFuture<Backend> getBackendAsync() {
return get().futureBackend;
}
public static RootShell getRootShell() {
@@ -115,11 +169,7 @@ public class Application extends android.app.Application {
public void onCreate() {
super.onCreate();
handler = new Handler(Looper.getMainLooper());
final Executor executor = AsyncTask.SERIAL_EXECUTOR;
final ConfigStore configStore = new FileConfigStore(getApplicationContext());
asyncWorker = new AsyncWorker(executor, handler);
asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
rootShell = new RootShell(getApplicationContext());
toolsInstaller = new ToolsInstaller(getApplicationContext());
@@ -128,8 +178,16 @@ public class Application extends android.app.Application {
sharedPreferences.getBoolean("dark_theme", false) ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
tunnelManager = new TunnelManager(configStore);
asyncWorker.runAsync(Application::getBackend);
tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
tunnelManager.onCreate();
asyncWorker.supplyAsync(Application::getBackend).thenAccept(backend -> {
futureBackend.complete(backend);
if (ACRA.isInitialised()) {
ACRA.getErrorReporter().putCustomData("backend", backend.getClass().getSimpleName());
asyncWorker.supplyAsync(backend::getVersion).thenAccept(version ->
ACRA.getErrorReporter().putCustomData("backendVersion", version));
}
});
}
}
@@ -20,7 +20,7 @@ public class BootShutdownReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
if (!(backend instanceof WgQuickBackend))
return;
final String action = intent.getAction();
@@ -14,6 +14,7 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.IBinder;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import android.support.annotation.Nullable;
@@ -44,9 +45,29 @@ public class QuickTileService extends TileService {
@Nullable private Icon iconOn;
@Nullable private Icon iconOff;
/* This works around an annoying unsolved frameworks bug some people are hitting. */
@Override
@Nullable
public IBinder onBind(final Intent intent) {
IBinder ret = null;
try {
ret = super.onBind(intent);
} catch (final Exception e) {
Log.d(TAG, "Failed to bind to TileService", e);
}
return ret;
}
/* TODO: for Android P: Build.VERSION_CODES.P */
private static final int BUILD_VERSION_CODES_P = 28;
@SuppressWarnings("deprecation")
@Override
public void onCreate() {
if (Build.VERSION.SDK_INT >= BUILD_VERSION_CODES_P) {
iconOff = iconOn = Icon.createWithResource(this, R.drawable.ic_tile);
return;
}
final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile));
icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */
icon.setSlashed(false);
@@ -55,17 +76,12 @@ public class QuickTileService extends TileService {
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
icon.draw(c);
iconOn = Icon.createWithBitmap(b);
/* TODO(msf): Change this to an explicit test for P when we start targetting SDK 28 */
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) {
iconOff = iconOn;
} else {
icon.setSlashed(true);
b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
c = new Canvas(b);
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
icon.draw(c);
iconOff = Icon.createWithBitmap(b);
}
icon.setSlashed(true);
b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
c = new Canvas(b);
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
icon.draw(c);
iconOff = Icon.createWithBitmap(b);
}
@Override
@@ -13,7 +13,6 @@ import android.support.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager;
import java.util.Objects;
@@ -39,15 +38,17 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
String savedTunnelName = null;
final String savedTunnelName;
if (savedInstanceState != null)
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
else if (getIntent() != null)
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
if (savedTunnelName != null) {
final TunnelManager tunnelManager = Application.getTunnelManager();
selectedTunnel = tunnelManager.getTunnels().get(savedTunnelName);
}
else
savedTunnelName = null;
if (savedTunnelName != null)
Application.getTunnelManager().getTunnels()
.thenAccept(tunnels -> setSelectedTunnel(tunnels.get(savedTunnelName)));
// The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState);
@@ -10,12 +10,13 @@ import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.support.v7.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import com.wireguard.android.R;
import com.wireguard.android.fragment.TunnelDetailFragment;
@@ -23,71 +24,46 @@ import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.fragment.TunnelListFragment;
import com.wireguard.android.model.Tunnel;
import java.util.List;
import java9.util.stream.Stream;
/**
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
* WireGuard application, and contains several fragments for listing, viewing details of, and
* editing the configuration and interface state of WireGuard tunnels.
*/
public class MainActivity extends BaseActivity {
private static final String KEY_STATE = "fragment_state";
private static final String TAG = "WireGuard/" + MainActivity.class.getSimpleName();
private State state = State.EMPTY;
private boolean moveToState(final State nextState) {
if (state == nextState)
return false;
final FragmentManager fragmentManager = getSupportFragmentManager();
Log.i(TAG, "Moving from " + state.name() + " to " + nextState.name());
if (nextState.layer > state.layer + 1) {
moveToState(State.ofLayer(state.layer + 1));
moveToState(nextState);
return true;
} else if (nextState.layer == state.layer + 1) {
final Fragment fragment = Fragment.instantiate(this, nextState.fragment);
final FragmentTransaction transaction = fragmentManager.beginTransaction()
.replace(R.id.master_fragment, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (state.layer > 0)
transaction.addToBackStack(null);
transaction.commitAllowingStateLoss(); /* TODO: switch back to .commit() when this function is rewritten. */
} else if (nextState.layer == state.layer - 1) {
if (fragmentManager.getBackStackEntryCount() == 0)
return false;
fragmentManager.popBackStack();
} else if (nextState.layer < state.layer - 1) {
moveToState(State.ofLayer(state.layer - 1));
moveToState(nextState);
return true;
}
state = nextState;
if (state.layer <= State.LIST.layer)
setSelectedTunnel(null);
updateActionBar();
return true;
}
public class MainActivity extends BaseActivity
implements FragmentManager.OnBackStackChangedListener {
@Nullable private ActionBar actionBar;
private boolean isTwoPaneLayout;
@Nullable private TunnelListFragment listFragment;
@Override
public void onBackPressed() {
final List<Fragment> fragments = getSupportFragmentManager().getFragments();
boolean handled = false;
if (!fragments.isEmpty() && fragments.get(0) instanceof TunnelListFragment) {
handled = ((TunnelListFragment) fragments.get(0)).collapseActionMenu();
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
// If the action menu is visible and expanded, collapse it instead of navigating back.
if (isTwoPaneLayout || backStackEntries == 0) {
if (listFragment != null && listFragment.collapseActionMenu())
return;
}
if (!handled) {
handled = moveToState(State.ofLayer(state.layer - 1));
// If the two-pane layout does not have an editor open, going back should exit the app.
if (isTwoPaneLayout && backStackEntries <= 1) {
finish();
return;
}
if (!handled) {
super.onBackPressed();
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
if (!isTwoPaneLayout && backStackEntries == 1) {
setSelectedTunnel(null);
return;
}
super.onBackPressed();
}
@Override public void onBackStackChanged() {
if (actionBar == null)
return;
// Do not show the home menu when the two-pane layout is at the detail view (see above).
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
final int minBackStackEntries = isTwoPaneLayout ? 2 : 1;
actionBar.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries);
}
// We use onTouchListener here to avoid the UI click sound, hence
@@ -97,25 +73,14 @@ public class MainActivity extends BaseActivity {
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
if (savedInstanceState != null && savedInstanceState.getString(KEY_STATE) != null)
state = State.valueOf(savedInstanceState.getString(KEY_STATE));
if (state == State.EMPTY) {
State initialState = getSelectedTunnel() != null ? State.DETAIL : State.LIST;
if (getIntent() != null && getIntent().getStringExtra(KEY_STATE) != null)
initialState = State.valueOf(getIntent().getStringExtra(KEY_STATE));
moveToState(initialState);
}
updateActionBar();
final int actionBarId = getResources().getIdentifier("action_bar", "id", getPackageName());
if (actionBarId != 0 && findViewById(actionBarId) != null) {
findViewById(actionBarId).setOnTouchListener((v, e) -> {
final List<Fragment> fragments = getSupportFragmentManager().getFragments();
if (!fragments.isEmpty() && fragments.get(0) instanceof TunnelListFragment) {
((TunnelListFragment) fragments.get(0)).collapseActionMenu();
}
return false;
});
}
actionBar = getSupportActionBar();
isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST");
getSupportFragmentManager().addOnBackStackChangedListener(this);
onBackStackChanged();
final View actionBarView = findViewById(R.id.action_bar);
if (actionBarView != null)
actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu());
}
@Override
@@ -129,11 +94,14 @@ public class MainActivity extends BaseActivity {
switch (item.getItemId()) {
case android.R.id.home:
// The back arrow in the action bar should act the same as the back button.
moveToState(State.ofLayer(state.layer - 1));
onBackPressed();
return true;
case R.id.menu_action_edit:
if (getSelectedTunnel() != null)
moveToState(State.EDITOR);
getSupportFragmentManager().beginTransaction()
.replace(R.id.detail_container, new TunnelEditorFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
return true;
case R.id.menu_action_save:
// This menu item is handled by the editor fragment.
@@ -147,37 +115,26 @@ public class MainActivity extends BaseActivity {
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
outState.putString(KEY_STATE, state.name());
super.onSaveInstanceState(outState);
}
@Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
moveToState(newTunnel != null ? State.DETAIL : State.LIST);
}
private void updateActionBar() {
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(state.layer > State.LIST.layer);
}
private enum State {
EMPTY(null, 0),
LIST(TunnelListFragment.class, 1),
DETAIL(TunnelDetailFragment.class, 2),
EDITOR(TunnelEditorFragment.class, 3);
@Nullable private final String fragment;
private final int layer;
State(@Nullable final Class<? extends Fragment> fragment, final int layer) {
this.fragment = fragment != null ? fragment.getName() : null;
this.layer = layer;
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
@Nullable final Tunnel newTunnel) {
final FragmentManager fragmentManager = getSupportFragmentManager();
final int backStackEntries = fragmentManager.getBackStackEntryCount();
if (newTunnel == null) {
// Clear everything off the back stack (all editors and detail fragments).
fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
return;
}
private static State ofLayer(final int layer) {
return Stream.of(State.values()).filter(s -> s.layer == layer).findFirst().get();
if (backStackEntries == 2) {
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
// method to avoid the editor picking up the new tunnel while it is still visible.
fragmentManager.popBackStackImmediate();
} else if (backStackEntries == 0) {
// Create and show a new detail fragment.
fragmentManager.beginTransaction()
.add(R.id.detail_container, new TunnelDetailFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
}
}
}
@@ -99,7 +99,7 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
for (final Preference pref : wgQuickOnlyPrefs)
pref.setVisible(false);
final PreferenceScreen screen = getPreferenceScreen();
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend instanceof WgQuickBackend)
pref.setVisible(true);
@@ -6,14 +6,49 @@
package com.wireguard.android.activity;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.util.Log;
import com.wireguard.android.Application;
import java.lang.reflect.Field;
public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
@Nullable private static Resources lastResources;
private static boolean lastDarkMode;
private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) {
if (resources == lastResources && darkMode == lastDarkMode)
return;
try {
Field f;
Object o = resources;
try {
f = o.getClass().getDeclaredField("mResourcesImpl");
f.setAccessible(true);
o = f.get(o);
} catch (final Exception ignored) { }
f = o.getClass().getDeclaredField("mDrawableCache");
f.setAccessible(true);
o = f.get(o);
try {
o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1);
} catch (final Exception ignored) {
o.getClass().getMethod("clear").invoke(o);
}
} catch (final Exception e) {
Log.e(TAG, "Failed to flush drawable cache", e);
}
lastResources = resources;
lastDarkMode = darkMode;
}
@Override
@@ -31,10 +66,12 @@ public abstract class ThemeChangeAwareActivity extends AppCompatActivity impleme
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if ("dark_theme".equals(key)) {
final boolean darkMode = sharedPreferences.getBoolean(key, false);
AppCompatDelegate.setDefaultNightMode(
sharedPreferences.getBoolean(key, false) ?
AppCompatDelegate.MODE_NIGHT_YES :
AppCompatDelegate.MODE_NIGHT_NO);
invalidateDrawableCache(getResources(), darkMode);
recreate();
}
}
@@ -240,10 +240,13 @@ public final class GoBackend implements Backend {
@Override
public void onDestroy() {
for (final Tunnel tunnel : Application.getTunnelManager().getTunnels()) {
if (tunnel != null && tunnel.getState() != State.DOWN)
tunnel.setState(State.DOWN);
}
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (final Tunnel tunnel : tunnels) {
if (tunnel != null && tunnel.getState() != State.DOWN)
tunnel.setState(State.DOWN);
}
});
vpnService = vpnService.newIncompleteFuture();
super.onDestroy();
}
@@ -27,6 +27,7 @@ import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.widget.MonkeyedSnackbar;
/**
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
@@ -94,7 +95,7 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC
if (tunnel == null)
return;
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
if (backend instanceof GoBackend) {
final Intent intent = GoBackend.VpnService.prepare(view.getContext());
if (intent != null) {
@@ -118,7 +119,7 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC
final String message = getContext().getString(messageResId, error);
final View view = getView();
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
MonkeyedSnackbar.make(view, message, Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
@@ -32,6 +32,7 @@ import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.widget.MonkeyedSnackbar;
import com.wireguard.config.Attribute;
import com.wireguard.config.Config;
import com.wireguard.config.Peer;
@@ -72,7 +73,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
message = getString(R.string.config_save_error, savedTunnel.getName(), error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@@ -174,6 +175,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
}
// Tell the activity to finish itself or go back to the detail view.
getActivity().runOnUiThread(() -> {
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
// The selected tunnel has to actually change, but we have to remember this one.
final Tunnel savedTunnel = tunnel;
if (savedTunnel == getSelectedTunnel())
@@ -194,7 +196,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
final String tunnelName = tunnel == null ? binding.getConfig().getName() : tunnel.getName();
final String message = getString(R.string.config_save_error, tunnelName, error);
Log.e(TAG, message, e);
Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
MonkeyedSnackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
return false;
}
if (tunnel == null) {
@@ -247,7 +249,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
message = getString(R.string.tunnel_create_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@@ -266,7 +268,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
message = getString(R.string.tunnel_rename_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@@ -21,6 +21,7 @@ import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -38,6 +39,8 @@ import com.wireguard.android.databinding.TunnelListFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.android.widget.MonkeyedSnackbar;
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
import com.wireguard.config.Config;
@@ -85,7 +88,7 @@ public class TunnelListFragment extends BaseFragment {
final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null)
ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
} catch (final IllegalArgumentException|IOException exception) {
} catch (final Exception exception) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
}
}
@@ -262,7 +265,28 @@ public class TunnelListFragment extends BaseFragment {
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
// Do nothing.
if (binding == null)
return;
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
if (newTunnel != null)
binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(newTunnel)).itemView.setActivated(true);
if (oldTunnel != null)
binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(oldTunnel)).itemView.setActivated(false);
});
/* Alternative 1: results in sluggish change:
if (binding.tunnelList.getAdapter() == null)
return;
* Alternative 2: results in overly quick change:
binding.tunnelList.getAdapter().notifyDataSetChanged();
* Hence, we go with the above.
*/
}
private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
@@ -275,7 +299,7 @@ public class TunnelListFragment extends BaseFragment {
Log.e(TAG, message, throwable);
}
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
@@ -301,7 +325,27 @@ public class TunnelListFragment extends BaseFragment {
tunnels.size(), tunnels.size() + throwables.size());
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
}
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
if (checkedItems != null) {
for (final Integer i : checkedItems)
actionModeListener.setItemChecked(i, true);
}
}
}
@Override
@@ -313,7 +357,7 @@ public class TunnelListFragment extends BaseFragment {
}
binding.setFragment(this);
binding.setTunnels(Application.getTunnelManager().getTunnels());
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
binding.setFragment(this);
binding.getRoot().setOnClickListener(clicked -> {
@@ -328,7 +372,11 @@ public class TunnelListFragment extends BaseFragment {
return true;
});
binding.getRoot().setActivated(actionModeListener.checkedItems.contains(position));
if (actionMode != null)
binding.getRoot().setActivated(actionModeListener.checkedItems.contains(position));
else
binding.getRoot().setActivated(getSelectedTunnel() == tunnel);
});
}
@@ -341,25 +389,29 @@ public class TunnelListFragment extends BaseFragment {
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_delete:
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : checkedItems) {
tunnelsToDelete.add(Application.getTunnelManager().getTunnels().get(position));
}
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : copyCheckedItems)
tunnelsToDelete.add(tunnels.get(position));
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::delete)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures)
.thenApply(x -> futures.length)
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::delete)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures)
.thenApply(x -> futures.length)
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
});
checkedItems.clear();
mode.finish();
return true;
case R.id.menu_action_select_all:
for (int i = 0; i < Application.getTunnelManager().getTunnels().size(); ++i) {
setItemChecked(i, true);
}
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (int i = 0; i < tunnels.size(); ++i) {
setItemChecked(i, true);
}
});
return true;
default:
return false;
@@ -373,6 +425,7 @@ public class TunnelListFragment extends BaseFragment {
resources = getActivity().getResources();
}
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
binding.tunnelList.getAdapter().notifyDataSetChanged();
return true;
}
@@ -389,6 +442,10 @@ public class TunnelListFragment extends BaseFragment {
setItemChecked(position, !checkedItems.contains(position));
}
public ArrayList<Integer> getCheckedItems() {
return new ArrayList<>(checkedItems);
}
void setItemChecked(final int position, final boolean checked) {
if (checked) {
checkedItems.add(position);
@@ -396,15 +453,16 @@ public class TunnelListFragment extends BaseFragment {
checkedItems.remove(position);
}
final RecyclerView.Adapter adapter = binding == null ? null : binding.tunnelList.getAdapter();
if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
((AppCompatActivity) getActivity()).startSupportActionMode(this);
} else if (actionMode != null && checkedItems.isEmpty()) {
actionMode.finish();
}
if (binding != null) {
binding.tunnelList.getAdapter().notifyItemChanged(position);
}
if (adapter != null)
adapter.notifyItemChanged(position);
updateTitle(actionMode);
}
@@ -19,7 +19,6 @@ import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.config.Config;
@@ -47,6 +46,7 @@ public final class TunnelManager extends BaseObservable {
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
private final ConfigStore configStore;
private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>();
private final ObservableSortedKeyedList<String, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
@Nullable private Tunnel lastUsedTunnel;
private boolean haveLoaded;
@@ -121,8 +121,8 @@ public final class TunnelManager extends BaseObservable {
.thenApply(tunnel::onStatisticsChanged);
}
public ObservableKeyedList<String, Tunnel> getTunnels() {
return tunnels;
public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() {
return completableTunnels;
}
public void onCreate() {
@@ -152,6 +152,8 @@ public final class TunnelManager extends BaseObservable {
f.completeExceptionally(t);
}
});
completableTunnels.complete(tunnels);
}
public void refreshTunnelStates() {
@@ -285,10 +287,12 @@ public final class TunnelManager extends BaseObservable {
final String tunnelName = intent.getStringExtra("tunnel");
if (tunnelName == null)
return;
final Tunnel tunnel = manager.getTunnels().get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
manager.getTunnels().thenAccept(tunnels -> {
final Tunnel tunnel = tunnels.get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
});
}
}
}
@@ -20,6 +20,7 @@ import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.FragmentUtils;
import com.wireguard.android.widget.MonkeyedSnackbar;
import java.io.BufferedReader;
import java.io.File;
@@ -79,7 +80,7 @@ public class LogExporterPreference extends Preference {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String message = getContext().getString(R.string.log_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
MonkeyedSnackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
@@ -23,7 +23,7 @@ public class VersionPreference extends Preference {
public VersionPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase());
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
versionSummary = exception == null
@@ -21,6 +21,7 @@ import com.wireguard.android.R;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.FragmentUtils;
import com.wireguard.android.widget.MonkeyedSnackbar;
import com.wireguard.config.Config;
import java.io.File;
@@ -48,7 +49,10 @@ public class ZipExporterPreference extends Preference {
}
private void exportZip() {
final List<Tunnel> tunnels = new ArrayList<>(Application.getTunnelManager().getTunnels());
Application.getTunnelManager().getTunnels().thenAccept(this::exportZip);
}
private void exportZip(final List<Tunnel> tunnels) {
final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
for (final Tunnel tunnel : tunnels)
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
@@ -85,7 +89,7 @@ public class ZipExporterPreference extends Preference {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String message = getContext().getString(R.string.zip_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
MonkeyedSnackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
@@ -13,6 +13,8 @@ import android.support.design.widget.Snackbar;
import android.view.View;
import android.widget.TextView;
import com.wireguard.android.widget.MonkeyedSnackbar;
/**
* Standalone utilities for interacting with the system clipboard.
*/
@@ -33,6 +35,6 @@ public final class ClipboardUtils {
return;
final CharSequence description = view.getContentDescription();
((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text));
Snackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
MonkeyedSnackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
}
}
@@ -0,0 +1,33 @@
package com.wireguard.android.widget;
import android.support.annotation.NonNull;
import android.support.design.widget.BaseTransientBottomBar;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import com.wireguard.android.BootShutdownReceiver;
import java.lang.reflect.Field;
public class MonkeyedSnackbar {
private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
public static Snackbar make(@NonNull final View view, @NonNull final CharSequence text,
@BaseTransientBottomBar.Duration final int duration) {
final Snackbar snackbar = Snackbar.make(view, text, duration);
try {
final Field accessibilityManager = Snackbar.class.getSuperclass().getDeclaredField("mAccessibilityManager");
accessibilityManager.setAccessible(true);
final Field isEnabled = AccessibilityManager.class.getDeclaredField("mIsEnabled");
isEnabled.setAccessible(true);
isEnabled.setBoolean(accessibilityManager.get(snackbar), false);
} catch (final Exception e) {
Log.e(TAG, "Unable to force-enable snackbar animations", e);
}
return snackbar;
}
}
@@ -6,15 +6,22 @@
package com.wireguard.android.widget.fab;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) { }
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
@@ -22,10 +29,39 @@ public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<Flo
return dependency instanceof Snackbar.SnackbarLayout;
}
private static final long ANIMATION_DURATION = 250;
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) {
final float origin = child.getBehaviorYTranslation();
if (Math.abs(destination - origin) < fullSpan / 2) {
child.setBehaviorYTranslation(destination);
return;
}
final ValueAnimator animator = new ValueAnimator();
animator.setFloatValues(origin, destination);
animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration((long)(ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan)));
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator a) {
child.setBehaviorYTranslation(destination);
}
});
animator.addUpdateListener(a -> child.setBehaviorYTranslation((float)a.getAnimatedValue()));
animator.start();
}
@Override
public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
child.setBehaviorYTranslation(Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()));
animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight());
return true;
}
@Override
public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
animateChange(child, 0, dependency.getMeasuredHeight());
}
}
@@ -55,6 +55,7 @@ public enum Attribute {
return TextUtils.join(", ", iterable);
}
@Nullable
public static Attribute match(final CharSequence line) {
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0].toLowerCase());
}
@@ -140,6 +140,8 @@ public class Interface {
public void parse(final String line) {
final Attribute key = Attribute.match(line);
if (key == null)
throw new IllegalArgumentException(String.format("Unable to parse line: \"%s\"", line));
switch (key) {
case ADDRESS:
addAddresses(key.parseList(line));
@@ -113,6 +113,8 @@ public class Peer {
public void parse(final String line) {
final Attribute key = Attribute.match(line);
if (key == null)
throw new IllegalArgumentException(String.format("Unable to parse line: \"%s\"", line));
switch (key) {
case ALLOWED_IPS:
addAllowedIPs(key.parseList(line));
@@ -5,6 +5,8 @@
package com.wireguard.util;
import android.support.annotation.Nullable;
import java.util.Collection;
import java.util.List;
@@ -18,8 +20,10 @@ public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
boolean containsKey(K key);
@Nullable
E get(K key);
@Nullable
E getLast(K key);
int indexOfKey(K key);
@@ -5,6 +5,8 @@
package com.wireguard.util;
import android.support.annotation.Nullable;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
@@ -17,10 +19,12 @@ import java.util.Set;
public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
Comparator<? super K> comparator();
@Nullable
K firstKey();
Set<K> keySet();
@Nullable
K lastKey();
Collection<E> values();
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/list_item_activated" android:state_activated="true" /> <!-- TODO(msf): themeify this -->
<item android:drawable="@android:color/transparent" /> <!-- TODO(msf): themeify this -->
<!-- TODO(msf): somehow make an additional activated color entry here (or elsewhere?) that
is in use only in multiselect mode, and one that's in use only in ordinary
select mode. In otherwords, we don't want to use the same background color
for both multiselect and normal select (on two column machines).
-->
<item android:drawable="@color/list_item_activated" android:state_activated="true" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
<item android:drawable="@android:color/transparent" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
</selector>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/list_item_ripple"> <!-- TODO(msf): themeify this -->
android:color="@color/list_item_ripple"> <!-- TODO(msf): get color from theme with ?android/attr: -->
<item android:drawable="@drawable/list_item_background" />
<item android:id="@android:id/mask" android:drawable="@android:color/white" />
<item android:id="@android:id/mask" android:drawable="@android:color/white" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
</ripple>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:divider="?android:attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
tools:context=".activity.MainActivity">
<fragment
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:tag="LIST" />
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.wireguard.android.widget.NameInputFilter" />
</data>
<FrameLayout
android:layout_width="match_parent"
@@ -15,7 +20,9 @@
android:id="@+id/tunnel_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/tunnel_name" />
android:hint="@string/tunnel_name"
android:inputType="textNoSuggestions|textVisiblePassword"
app:filter="@{NameInputFilter.newInstance()}" />
</android.support.design.widget.TextInputLayout>
+14 -2
View File
@@ -1,7 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_fragment"
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame" />
tools:context=".activity.MainActivity">
<fragment
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="LIST" />
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>