Compare commits

..

8 Commits

Author SHA1 Message Date
Jason A. Donenfeld 1f7bdd4f5f Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-10 22:00:10 +02:00
Jason A. Donenfeld 3cf6aad083 QuickTileService: automatically slash the tile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-09 18:55:55 +02:00
Jason A. Donenfeld b997a2581b BaseFragment: in the event no view is available, use toast
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-09 17:29:35 +02:00
Eric Kuck d7ea078cdf Request VPN permissions on activation
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-09 17:08:41 +02:00
Jason A. Donenfeld d50e0f5fb9 Use instanceOf instead of getClass
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-08 23:59:25 +02:00
Eric Kuck c696e9f275 Build with different name and ID in debug mode
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-08 03:26:42 +02:00
Jason A. Donenfeld 707c8c19a8 gradle: bump build tools version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-08 03:18:15 +02:00
Eric Kuck b37b48b8dc Switch from ListView to RecyclerView
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-08 02:50:49 +02:00
22 changed files with 472 additions and 354 deletions
+7 -3
View File
@@ -5,7 +5,7 @@ apply plugin: 'com.android.application'
final def keystorePropertiesFile = rootProject.file("keystore.properties")
android {
buildToolsVersion '28.0.0'
buildToolsVersion '28.0.1'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@@ -16,8 +16,8 @@ android {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 27
versionCode 429
versionName '0.0.20180706'
versionCode 430
versionName '0.0.20180710'
}
// If the keystore file exists
if (keystorePropertiesFile.exists()) {
@@ -40,6 +40,10 @@ android {
release {
if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
}
}
externalNativeBuild {
cmake {
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WireGuard β</string>
</resources>
+2 -2
View File
@@ -9,7 +9,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<permission
android:name="com.wireguard.android.permission.CONTROL_TUNNELS"
android:name="${applicationId}.permission.CONTROL_TUNNELS"
android:protectionLevel="dangerous"
android:icon="@mipmap/ic_launcher"
android:label="@string/permission_label"
@@ -59,7 +59,7 @@
<receiver
android:name=".model.TunnelManager$IntentReceiver"
android:permission="com.wireguard.android.permission.CONTROL_TUNNELS">
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
<action android:name="com.wireguard.android.action.SET_TUNNEL_UP" />
@@ -25,7 +25,7 @@ import com.wireguard.android.util.ToolsInstaller;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Collection;
import java.util.concurrent.Executor;
public class Application extends android.app.Application {
@@ -37,7 +37,7 @@ public class Application extends android.app.Application {
private ToolsInstaller toolsInstaller;
private TunnelManager tunnelManager;
private Handler handler;
private List<BackendCallback> haveBackendCallbacks = new ArrayList<>();
private Collection<BackendCallback> haveBackendCallbacks = new ArrayList<>();
private final Object haveBackendCallbacksLock = new Object();
public Application() {
@@ -21,7 +21,7 @@ public class BootShutdownReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
Application.onHaveBackend(backend -> {
if (backend.getClass() != WgQuickBackend.class)
if (!(backend instanceof WgQuickBackend))
return;
final String action = intent.getAction();
if (action == null)
@@ -10,6 +10,10 @@ import android.annotation.TargetApi;
import android.content.Intent;
import android.databinding.Observable;
import android.databinding.Observable.OnPropertyChangedCallback;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.service.quicksettings.Tile;
@@ -21,6 +25,7 @@ import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.widget.SlashDrawable;
import java.util.Objects;
@@ -37,6 +42,26 @@ public class QuickTileService extends TileService {
private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
private Tunnel tunnel;
private Icon iconOn;
private Icon iconOff;
@Override
public void onCreate() {
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);
Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
icon.draw(c);
iconOn = 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
public void onClick() {
@@ -99,10 +124,7 @@ public class QuickTileService extends TileService {
return;
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;
tile.setIcon(Icon.createWithResource(this, iconResource));
tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff);
tile.setState(state);
}
tile.updateTile();
@@ -6,13 +6,11 @@
package com.wireguard.android.activity;
import android.content.Intent;
import android.databinding.CallbackRegistry;
import android.databinding.CallbackRegistry.NotifierCallback;
import android.os.Bundle;
import com.wireguard.android.Application;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager;
@@ -52,14 +50,6 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
// The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState);
Application.onHaveBackend(backend -> {
if (backend.getClass() == GoBackend.class) {
final Intent intent = GoBackend.VpnService.prepare(this);
if (intent != null)
startActivityForResult(intent, 0);
}
});
}
@Override
@@ -102,7 +102,7 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
final PreferenceScreen screen = getPreferenceScreen();
Application.onHaveBackend(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend.getClass() == WgQuickBackend.class)
if (backend instanceof WgQuickBackend)
pref.setVisible(true);
else
screen.removePreference(pref);
@@ -13,10 +13,10 @@ import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.InputFilter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import com.wireguard.android.R;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.android.widget.ToggleSwitch;
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
@@ -67,37 +67,11 @@ public final class BindingAdapters {
listener.setList(newList);
}
@BindingAdapter({"items", "layout"})
public static <K, E extends Keyed<? extends K>>
void setItems(final ListView view,
final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
final ObservableKeyedList<K, E> newList, final int newLayoutId) {
if (oldList == newList && oldLayoutId == newLayoutId)
return;
// The ListAdapter interface is not generic, so this cannot be checked.
@SuppressWarnings("unchecked") ObservableKeyedListAdapter<K, E> adapter =
(ObservableKeyedListAdapter<K, E>) view.getAdapter();
// If the layout changes, any existing adapter must be replaced.
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
adapter.setList(null);
adapter = null;
}
// Avoid setting an adapter when there is no new list or layout.
if (newList == null || newLayoutId == 0)
return;
if (adapter == null) {
adapter = new ObservableKeyedListAdapter<>(view.getContext(), newLayoutId, newList);
view.setAdapter(adapter);
}
// Either the list changed, or this is an entirely new listener because the layout changed.
adapter.setList(newList);
}
@BindingAdapter({"items", "layout"})
@BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
public static <K, E extends Keyed<? extends K>>
void setItems(final RecyclerView view,
final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
final ObservableKeyedList<K, E> newList, final int newLayoutId) {
final ObservableKeyedList<K, E> oldList, final int oldLayoutId, final RowConfigurationHandler oldRowConfigurationHandler,
final ObservableKeyedList<K, E> newList, final int newLayoutId, final RowConfigurationHandler newRowConfigurationHandler) {
if (view.getLayoutManager() == null)
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
@@ -118,6 +92,8 @@ public final class BindingAdapters {
adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList);
view.setAdapter(adapter);
}
adapter.setRowConfigurationHandler(newRowConfigurationHandler);
// Either the list changed, or this is an entirely new listener because the layout changed.
adapter.setList(newList);
}
@@ -1,133 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import android.content.Context;
import android.databinding.DataBindingUtil;
import android.databinding.ObservableList;
import android.databinding.ViewDataBinding;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import com.wireguard.android.BR;
import com.wireguard.util.Keyed;
import com.wireguard.android.util.ObservableKeyedList;
import java.lang.ref.WeakReference;
/**
* A generic {@code ListAdapter} backed by a {@code ObservableKeyedList}.
*/
class ObservableKeyedListAdapter<K, E extends Keyed<? extends K>> extends BaseAdapter {
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
private final int layoutId;
private final LayoutInflater layoutInflater;
private ObservableKeyedList<K, E> list;
ObservableKeyedListAdapter(final Context context, final int layoutId,
final ObservableKeyedList<K, E> list) {
this.layoutId = layoutId;
layoutInflater = LayoutInflater.from(context);
setList(list);
}
@Override
public int getCount() {
return list != null ? list.size() : 0;
}
@Override
public E getItem(final int position) {
if (list == null || position < 0 || position >= list.size())
return null;
return list.get(position);
}
@Override
public long getItemId(final int position) {
final K key = getKey(position);
return key != null ? key.hashCode() : -1;
}
private K getKey(final int position) {
final E item = getItem(position);
return item != null ? item.getKey() : null;
}
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
if (binding == null)
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
binding.setVariable(BR.collection, list);
binding.setVariable(BR.key, getKey(position));
binding.setVariable(BR.item, getItem(position));
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public boolean hasStableIds() {
return true;
}
void setList(final ObservableKeyedList<K, E> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
if (list != null) {
list.addOnListChangedCallback(callback);
}
notifyDataSetChanged();
}
private static final class OnListChangedCallback<E extends Keyed<?>>
extends ObservableList.OnListChangedCallback<ObservableList<E>> {
private final WeakReference<ObservableKeyedListAdapter<?, E>> weakAdapter;
private OnListChangedCallback(final ObservableKeyedListAdapter<?, E> adapter) {
weakAdapter = new WeakReference<>(adapter);
}
@Override
public void onChanged(final ObservableList<E> sender) {
final ObservableKeyedListAdapter adapter = weakAdapter.get();
if (adapter != null)
adapter.notifyDataSetChanged();
else
sender.removeOnListChangedCallback(this);
}
@Override
public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition,
final int toPosition, final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
}
}
@@ -26,12 +26,13 @@ import java.lang.ref.WeakReference;
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
*/
class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
private final int layoutId;
private final LayoutInflater layoutInflater;
private ObservableKeyedList<K, E> list;
private RowConfigurationHandler rowConfigurationHandler;
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
final ObservableKeyedList<K, E> list) {
@@ -67,12 +68,17 @@ class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extend
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
}
@SuppressWarnings("unchecked")
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
holder.binding.setVariable(BR.collection, list);
holder.binding.setVariable(BR.key, getKey(position));
holder.binding.setVariable(BR.item, getItem(position));
holder.binding.executePendingBindings();
if (rowConfigurationHandler != null) {
rowConfigurationHandler.onConfigureRow(holder.binding, getItem(position), position);
}
}
void setList(final ObservableKeyedList<K, E> newList) {
@@ -85,6 +91,10 @@ class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extend
notifyDataSetChanged();
}
void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) {
this.rowConfigurationHandler = rowConfigurationHandler;
}
private static final class OnListChangedCallback<E extends Keyed<?>>
extends ObservableList.OnListChangedCallback<ObservableList<E>> {
@@ -138,4 +148,8 @@ class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extend
}
}
public interface RowConfigurationHandler<B extends ViewDataBinding, T> {
void onConfigureRow(B binding, T item, int position);
}
}
@@ -7,11 +7,26 @@
package com.wireguard.android.fragment;
import android.content.Context;
import android.content.Intent;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.BaseActivity;
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
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;
/**
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
@@ -19,7 +34,12 @@ import com.wireguard.android.model.Tunnel;
*/
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
private BaseActivity activity;
private Tunnel pendingTunnel;
private Boolean pendingTunnelUp;
protected Tunnel getSelectedTunnel() {
return activity != null ? activity.getSelectedTunnel() : null;
@@ -44,8 +64,62 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC
super.onDetach();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
if (pendingTunnel != null && pendingTunnelUp != null)
setTunnelStateWithPermissionsResult(pendingTunnel, pendingTunnelUp);
pendingTunnel = null;
pendingTunnelUp = null;
}
}
protected void setSelectedTunnel(final Tunnel tunnel) {
if (activity != null)
activity.setSelectedTunnel(tunnel);
}
public void setTunnelState(final View view, final boolean checked) {
final ViewDataBinding binding = DataBindingUtil.findBinding(view);
final Tunnel tunnel;
if (binding instanceof TunnelDetailFragmentBinding)
tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
else if (binding instanceof TunnelListItemBinding)
tunnel = ((TunnelListItemBinding) binding).getItem();
else
return;
Application.onHaveBackend(backend -> {
if (backend instanceof GoBackend) {
final Intent intent = GoBackend.VpnService.prepare(view.getContext());
if (intent != null) {
pendingTunnel = tunnel;
pendingTunnelUp = checked;
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
return;
}
}
setTunnelStateWithPermissionsResult(tunnel, checked);
});
}
private void setTunnelStateWithPermissionsResult(@NonNull final Tunnel tunnel, final boolean checked) {
tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
if (throwable == null)
return;
final String error = ExceptionLoggers.unwrapMessage(throwable);
final int messageResId = checked ? R.string.error_up : R.string.error_down;
final String message = getContext().getString(messageResId, error);
final View view = getView();
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
});
}
}
@@ -1,58 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.content.Context;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.View;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
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;
/**
* Helper method shared by TunnelListFragment and TunnelDetailFragment.
*/
public final class TunnelController {
private static final String TAG = "WireGuard/" + TunnelController.class.getSimpleName();
private TunnelController() {
// Prevent instantiation.
}
public static void setTunnelState(final View view, final boolean checked) {
final ViewDataBinding binding = DataBindingUtil.findBinding(view);
final Tunnel tunnel;
if (binding instanceof TunnelDetailFragmentBinding)
tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
else if (binding instanceof TunnelListItemBinding)
tunnel = ((TunnelListItemBinding) binding).getItem();
else
tunnel = null;
if (tunnel == null) {
Log.e(TAG, "setChecked() from a null tunnel", new IllegalStateException("No tunnel"));
return;
}
tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
if (throwable == null)
return;
final Context context = view.getContext();
final String error = ExceptionLoggers.unwrapMessage(throwable);
final int messageResId = checked ? R.string.error_up : R.string.error_down;
final String message = context.getString(messageResId, error);
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
});
}
}
@@ -69,7 +69,9 @@ public class TunnelDetailFragment extends BaseFragment {
@Override
public void onViewStateRestored(final Bundle savedInstanceState) {
binding.setFragment(this);
onSelectedTunnelChanged(null, getSelectedTunnel());
super.onViewStateRestored(savedInstanceState);
}
}
@@ -16,29 +16,26 @@ import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.TunnelCreatorActivity;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
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.widget.ToggleSwitch;
import com.wireguard.config.Config;
import java.io.BufferedReader;
@@ -47,13 +44,13 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java9.util.concurrent.CompletableFuture;
import java9.util.stream.Collectors;
import java9.util.stream.IntStream;
import java9.util.stream.StreamSupport;
/**
@@ -64,8 +61,7 @@ public class TunnelListFragment extends BaseFragment {
private static final int REQUEST_IMPORT = 1;
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
private final MultiChoiceModeListener actionModeListener = new ActionModeListener();
private final ListViewCallbacks listViewCallbacks = new ListViewCallbacks();
private final ActionModeListener actionModeListener = new ActionModeListener();
private ActionMode actionMode;
private TunnelListFragmentBinding binding;
@@ -182,20 +178,19 @@ public class TunnelListFragment extends BaseFragment {
}
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
binding.tunnelList.setMultiChoiceModeListener(actionModeListener);
binding.tunnelList.setOnItemClickListener(listViewCallbacks);
binding.tunnelList.setOnItemLongClickListener(listViewCallbacks);
binding.tunnelList.setOnTouchListener(listViewCallbacks);
binding.tunnelList.setOnTouchListener((view, motionEvent) -> {
if (binding != null) {
binding.createMenu.collapse();
}
return false;
});
binding.executePendingBindings();
return binding.getRoot();
}
@@ -276,38 +271,52 @@ public class TunnelListFragment extends BaseFragment {
super.onViewStateRestored(savedInstanceState);
binding.setFragment(this);
binding.setTunnels(Application.getTunnelManager().getTunnels());
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
binding.setFragment(this);
binding.getRoot().setOnClickListener(clicked -> {
if (actionMode == null) {
setSelectedTunnel(tunnel);
} else {
actionModeListener.toggleItemChecked(position);
}
});
binding.getRoot().setOnLongClickListener(clicked -> {
actionModeListener.toggleItemChecked(position);
return true;
});
binding.getRoot().setActivated(actionModeListener.checkedItems.contains(position));
});
}
private final class ActionModeListener implements MultiChoiceModeListener {
private Resources resources;
private AbsListView tunnelList;
private final class ActionModeListener implements ActionMode.Callback {
private final Set<Integer> checkedItems = new HashSet<>();
private IntStream getCheckedPositions() {
final SparseBooleanArray checkedItemPositions = tunnelList.getCheckedItemPositions();
return IntStream.range(0, checkedItemPositions.size())
.filter(checkedItemPositions::valueAt)
.map(checkedItemPositions::keyAt);
}
private Resources resources;
@Override
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_delete:
// Must operate in two steps: positions change once we start deleting things.
final List<Tunnel> tunnelsToDelete = getCheckedPositions()
.mapToObj(pos -> (Tunnel) tunnelList.getItemAtPosition(pos))
.collect(Collectors.toList());
List<Tunnel> tunnelsToDelete = new ArrayList<>();
for (Integer position : checkedItems) {
tunnelsToDelete.add(Application.getTunnelManager().getTunnels().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);
checkedItems.clear();
mode.finish();
return true;
case R.id.menu_action_select_all:
for (int i = 0; i < tunnelList.getAdapter().getCount(); ++i)
tunnelList.setItemChecked(i, true);
for (int i = 0; i < Application.getTunnelManager().getTunnels().size(); ++i) {
setItemChecked(i, true);
}
return true;
default:
return false;
@@ -317,9 +326,9 @@ public class TunnelListFragment extends BaseFragment {
@Override
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
actionMode = mode;
if (getActivity() != null)
if (getActivity() != null) {
resources = getActivity().getResources();
tunnelList = binding.tunnelList;
}
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
return true;
}
@@ -328,12 +337,31 @@ public class TunnelListFragment extends BaseFragment {
public void onDestroyActionMode(final ActionMode mode) {
actionMode = null;
resources = null;
checkedItems.clear();
binding.tunnelList.getAdapter().notifyDataSetChanged();
}
@Override
public void onItemCheckedStateChanged(final ActionMode mode, final int position,
final long id, final boolean checked) {
updateTitle(mode);
void toggleItemChecked(int position) {
setItemChecked(position, !checkedItems.contains(position));
}
void setItemChecked(int position, boolean checked) {
if (checked) {
checkedItems.add(position);
} else {
checkedItems.remove(position);
}
if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
((AppCompatActivity) getActivity()).startSupportActionMode(this);
} else if (actionMode != null && checkedItems.isEmpty()) {
actionMode.finish();
}
binding.tunnelList.getAdapter().notifyItemChanged(position);
updateTitle(actionMode);
}
@Override
@@ -342,8 +370,12 @@ public class TunnelListFragment extends BaseFragment {
return false;
}
private void updateTitle(final ActionMode mode) {
final int count = (int) getCheckedPositions().count();
private void updateTitle(@Nullable final ActionMode mode) {
if (mode == null) {
return;
}
final int count = checkedItems.size();
if (count == 0) {
mode.setTitle("");
} else {
@@ -352,30 +384,4 @@ public class TunnelListFragment extends BaseFragment {
}
}
private final class ListViewCallbacks
implements OnItemClickListener, OnItemLongClickListener, OnTouchListener {
@Override
public void onItemClick(final AdapterView<?> parent, final View view,
final int position, final long id) {
setSelectedTunnel((Tunnel) parent.getItemAtPosition(position));
}
@Override
public boolean onItemLongClick(final AdapterView<?> parent, final View view,
final int position, final long id) {
if (actionMode != null)
return false;
if (binding != null)
binding.tunnelList.setItemChecked(position, true);
return true;
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouch(final View view, final MotionEvent motionEvent) {
if (binding != null)
binding.createMenu.collapse();
return false;
}
}
}
@@ -0,0 +1,236 @@
/*
* Copyright © 2018 The Android Open Source Project
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.FloatProperty;
@TargetApi(Build.VERSION_CODES.N)
public class SlashDrawable extends Drawable {
private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
private static final long QS_ANIM_LENGTH = 350;
private final Path mPath = new Path();
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// These values are derived in un-rotated (vertical) orientation
private static final float SLASH_WIDTH = 1.8384776f;
private static final float SLASH_HEIGHT = 28f;
private static final float CENTER_X = 10.65f;
private static final float CENTER_Y = 11.869239f;
private static final float SCALE = 24f;
// Bottom is derived during animation
private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
// Draw the slash washington-monument style; rotate to no-u-turn style
private static final float DEFAULT_ROTATION = -45f;
private Drawable mDrawable;
private final RectF mSlashRect = new RectF(0, 0, 0, 0);
private float mRotation;
private boolean mSlashed;
private Mode mTintMode;
private ColorStateList mTintList;
private boolean mAnimationEnabled = true;
public SlashDrawable(final Drawable d) {
setDrawable(d);
}
@Override
public int getIntrinsicHeight() {
return mDrawable != null ? mDrawable.getIntrinsicHeight(): 0;
}
@Override
public int getIntrinsicWidth() {
return mDrawable != null ? mDrawable.getIntrinsicWidth(): 0;
}
@Override
protected void onBoundsChange(final Rect bounds) {
super.onBoundsChange(bounds);
mDrawable.setBounds(bounds);
}
public void setDrawable(final Drawable d) {
mDrawable = d;
mDrawable.setCallback(getCallback());
mDrawable.setBounds(getBounds());
if (mTintMode != null)
mDrawable.setTintMode(mTintMode);
if (mTintList != null)
mDrawable.setTintList(mTintList);
invalidateSelf();
}
public void setRotation(final float rotation) {
if (mRotation == rotation)
return;
mRotation = rotation;
invalidateSelf();
}
public void setAnimationEnabled(final boolean enabled) {
mAnimationEnabled = enabled;
}
// Animate this value on change
private float mCurrentSlashLength;
private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
@Override
public void setValue(final SlashDrawable object, final float value) {
object.mCurrentSlashLength = value;
}
@Override
public Float get(final SlashDrawable object) {
return object.mCurrentSlashLength;
}
};
public void setSlashed(final boolean slashed) {
if (mSlashed == slashed) return;
mSlashed = slashed;
final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
if (mAnimationEnabled) {
final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
anim.setDuration(QS_ANIM_LENGTH);
anim.start();
} else {
mCurrentSlashLength = end;
invalidateSelf();
}
}
@Override
public void draw(@NonNull final Canvas canvas) {
canvas.save();
final Matrix m = new Matrix();
final int width = getBounds().width();
final int height = getBounds().height();
final float radiusX = scale(CORNER_RADIUS, width);
final float radiusY = scale(CORNER_RADIUS, height);
updateRect(
scale(LEFT, width),
scale(TOP, height),
scale(RIGHT, width),
scale(TOP + mCurrentSlashLength, height)
);
mPath.reset();
// Draw the slash vertically
mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
// Rotate -45 + desired rotation
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
canvas.drawPath(mPath, mPaint);
// Rotate back to vertical
m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
// Draw another rect right next to the first, for clipping
m.setTranslate(mSlashRect.width(), 0);
mPath.transform(m);
mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
else
canvas.clipOutPath(mPath);
mDrawable.draw(canvas);
canvas.restore();
}
private float scale(final float frac, final int width) {
return frac * width;
}
private void updateRect(final float left, final float top, final float right, final float bottom) {
mSlashRect.left = left;
mSlashRect.top = top;
mSlashRect.right = right;
mSlashRect.bottom = bottom;
}
@Override
public void setTint(@ColorInt final int tintColor) {
super.setTint(tintColor);
mDrawable.setTint(tintColor);
mPaint.setColor(tintColor);
}
@Override
public void setTintList(@Nullable final ColorStateList tint) {
mTintList = tint;
super.setTintList(tint);
setDrawableTintList(tint);
mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
invalidateSelf();
}
private void setDrawableTintList(@Nullable final ColorStateList tint) {
mDrawable.setTintList(tint);
}
@Override
public void setTintMode(@NonNull final Mode tintMode) {
mTintMode = tintMode;
super.setTintMode(tintMode);
mDrawable.setTintMode(tintMode);
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
mDrawable.setAlpha(alpha);
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable final ColorFilter colorFilter) {
mDrawable.setColorFilter(colorFilter);
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
}
@@ -1,29 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="400dp"
android:height="400dp"
android:viewportHeight="400.0"
android:viewportWidth="400.0">
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M197.7,0C191.5,0.1 185.2,0.5 178.7,1.1C191.1,4 202.2,6.6 213.4,9.2C213.3,9.8 213.1,10.5 213,11.2C198.1,13.2 183.9,7.7 169.3,5.7C174.6,8.8 180,11.7 185.5,14.2C191.2,16.7 197,18.8 202.8,21.2C195.4,27.5 188,28.8 178.7,26.7C173.7,25.6 168.3,25 163.1,25.2C157.8,25.5 152.4,26.8 147.5,30C152.7,32.7 157.5,34.8 162.1,37.5C163.9,38.6 166.1,40.4 166.6,42.4C167.8,46.9 168.2,51.8 168.9,56.5C160.4,57.5 145.4,66.1 142.4,71.7C155.5,74.3 169.7,71.2 182.2,79.6C178.1,82.8 168.5,86.6 165,89.3C169.3,90.4 179.4,89.9 183.3,89.6C186.6,89.4 188.2,89.3 189.5,90.4L228,120.6C232.1,123.8 248.4,139.3 252.7,149C256.3,157.3 256.8,164.4 256.8,166.1C256.8,170.7 256.2,177.9 253,186C252.7,186.9 252,188.3 251.2,189.8L280.8,219.4C288.3,207.7 293.4,194.5 296.3,180C299.6,163.2 299.4,146.5 291.7,130.5C285.8,118.3 276.1,109.4 265.8,101.3C255.1,93 243.7,85.4 233.1,77C230.2,74.8 228.3,70.8 226.9,67.3C226.4,65.8 228.2,61.7 229.4,61.5C236,60.3 242.6,59.7 249.3,59.4C256.9,59.2 264.6,59.4 272.3,59.5C274,59.5 276.2,59.3 277.2,60.3C281.2,64.2 284.3,61.6 287,59.1C289.4,56.9 291,54 292.9,51.6C291.7,51.4 289.4,50.9 287.1,50.8C279.4,50.6 271.7,50.7 264,50.5C262.6,50.4 261.3,49 259.9,48.2C261.3,47.6 262.8,46.6 264.2,46.6C277.5,46.5 290.8,46.5 304.1,46.5C304.2,39.6 294.9,30.1 286.7,27.5C286.6,28.5 286.5,29.3 286.5,30.3C278.3,30.5 270.3,30.3 263,26.4C261.1,25.4 259.8,23.1 258.3,21.5C256.3,19.3 254.7,16.6 252.3,15.2C247.4,12.3 242,10.3 236.9,7.8C224.3,1.7 211.3,-0.1 197.7,0zM249.6,29.4C250.3,29.4 250.9,29.6 251.6,30.2C252.6,31 253.6,31.9 254.8,33C253.3,33.8 252,34.5 250.8,35.1C249,36 247.6,35.4 246.5,34C245.6,32.8 245.5,31.7 246.8,30.7C247.7,30 248.6,29.4 249.6,29.4zM231.8,213C220.7,221.9 207.6,227.2 193.6,230.1C152.7,238.5 118.7,282.2 128.3,330.3C139.5,386.5 201.5,416.9 252.2,390.2C285,372.9 302.3,339.2 297.7,302.5C295.9,288.1 291,275.4 283.1,264.3L265.1,246.3C264.2,246.2 263.2,246.6 261.9,247.5C253.1,253.1 244.2,258.4 235.1,263.4C229.9,266.2 224.2,268.3 217.7,271.2C219.9,271.8 221,272.1 222.1,272.4C246.5,278.9 259.6,300.3 253.8,324.4C248.7,345.7 227,359.4 205.9,355.8C188.4,352.8 173.1,338.2 170.5,320.8C167.8,301.8 177.2,283.5 194.1,275.9C203.4,271.6 213,267.9 222.3,263.6C232.9,258.7 244.4,254.9 253.6,248.1C256.2,246.2 258.7,244.2 261,242.2L231.8,213z"
android:strokeColor="#00000000"
android:strokeWidth="1.3750788" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m97.9,307.6c-7.8,2 -15.4,4.9 -23.4,7.5 3.9,-26.3 34.7,-50.6 60.8,-47.8 -8.1,10.9 -11.8,23.3 -12.7,35.6 -8.7,1.6 -16.8,2.7 -24.8,4.7"
android:strokeColor="#00000000"
android:strokeWidth="1.3750788" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M177,111.6C175.7,111.6 174.4,111.7 173.1,111.7L247,185.6C248.1,183.2 249,180.6 249.9,177.9C252.2,170.4 251.8,159.3 248,152.1C234.7,126.6 206.6,111.3 177,111.6zM139.7,120.9C137.9,121.8 136.1,122.9 134.3,124C81.2,156.4 84.3,231 132.9,260.7C136.6,263 138.3,262.7 140.8,259.4C148.4,249.2 157.9,241.1 169,234.9C171.7,233.3 174.5,231.9 178.1,230C154,225.8 141.1,214.8 139.6,197.8C137.9,179.4 146.9,164.5 163.2,158.3C167,156.9 171,156.2 174.9,156.1L139.7,120.9zM212.5,193.7C212.5,195.8 212.3,197.9 211.8,200.1C209.9,210.5 203.6,218.1 195.8,224.8C208.5,221.9 219.6,216.9 228.6,209.7L212.5,193.7z"
android:strokeColor="#00000000"
android:strokeWidth="1.3750788" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M78.4,59.6l262,262l-21.3,21.3l-262,-262z"
android:strokeWidth="0.82064575" />
</vector>
@@ -2,4 +2,5 @@
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/list_item_ripple"> <!-- TODO(msf): themeify this -->
<item android:drawable="@drawable/list_item_background" />
<item android:id="@android:id/mask" android:drawable="@android:color/white" />
</ripple>
@@ -5,12 +5,14 @@
<data>
<import type="com.wireguard.android.fragment.TunnelController" />
<import type="com.wireguard.android.model.Tunnel.State" />
<import type="com.wireguard.android.util.ClipboardUtils" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelDetailFragment" />
<variable
name="tunnel"
type="com.wireguard.android.model.Tunnel" />
@@ -63,7 +65,7 @@
android:layout_alignBaseline="@+id/interface_title"
android:layout_alignParentEnd="true"
app:checked="@{tunnel.state == State.UP}"
app:onBeforeCheckedChanged="@{TunnelController::setTunnelState}" />
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
<TextView
android:id="@+id/interface_name_label"
@@ -10,6 +10,10 @@
name="fragment"
type="com.wireguard.android.fragment.TunnelListFragment" />
<variable
name="rowConfigurationHandler"
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
<variable
name="tunnels"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, Tunnel&gt;" />
@@ -21,13 +25,14 @@
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<ListView
<android.support.v7.widget.RecyclerView
android:id="@+id/tunnel_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="multipleChoiceModal"
app:items="@{tunnels}"
app:layout="@{@layout/tunnel_list_item}" />
app:layout="@{@layout/tunnel_list_item}"
app:configurationHandler="@{rowConfigurationHandler}" />
<com.wireguard.android.widget.fab.FloatingActionsMenu
android:id="@+id/create_menu"
+5 -3
View File
@@ -4,8 +4,6 @@
<data>
<import type="com.wireguard.android.fragment.TunnelController" />
<import type="com.wireguard.android.model.Tunnel" />
<import type="com.wireguard.android.model.Tunnel.State" />
@@ -21,6 +19,10 @@
<variable
name="item"
type="com.wireguard.android.model.Tunnel" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelListFragment" />
</data>
<RelativeLayout
@@ -49,6 +51,6 @@
android:layout_alignBaseline="@+id/tunnel_name"
android:layout_alignParentEnd="true"
app:checked="@{item.state == State.UP}"
app:onBeforeCheckedChanged="@{TunnelController::setTunnelState}" />
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
</RelativeLayout>
</layout>