summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEric Kuck <eric@bluelinelabs.com>2018-07-06 16:02:48 -0500
committerJason A. Donenfeld <Jason@zx2c4.com>2018-07-08 02:50:49 +0200
commitb37b48b8dc2ad2eb130a002feee84d1a73fad147 (patch)
tree70a2f5394e65f4becccab279ccf4e86b1a426fcf
parent2c7203ab8d88a1eb497d674110922271ec610374 (diff)
Switch from ListView to RecyclerView
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
-rw-r--r--app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java36
-rw-r--r--app/src/main/java/com/wireguard/android/databinding/ObservableKeyedListAdapter.java133
-rw-r--r--app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java17
-rw-r--r--app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java148
-rw-r--r--app/src/main/res/drawable/list_item_background_anim.xml1
-rw-r--r--app/src/main/res/layout/tunnel_list_fragment.xml9
6 files changed, 107 insertions, 237 deletions
diff --git a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java
index b7c44116..b3a8ae25 100644
--- a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java
+++ b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java
@@ -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);
}
diff --git a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedListAdapter.java b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedListAdapter.java
deleted file mode 100644
index 9cf9490f..00000000
--- a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedListAdapter.java
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java
index 4a5ac3d2..79168e48 100644
--- a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java
+++ b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java
@@ -14,6 +14,7 @@ import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.BR;
@@ -26,12 +27,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 +69,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.getRoot(), getItem(position), position);
+ }
}
void setList(final ObservableKeyedList<K, E> newList) {
@@ -85,6 +92,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 +149,8 @@ class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extend
}
}
+ public interface RowConfigurationHandler<T> {
+ void onConfigureRow(View view, T item, int position);
+ }
+
}
diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java
index 403e458b..66c1d7b9 100644
--- a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java
+++ b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java
@@ -16,26 +16,21 @@ 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.model.Tunnel;
import com.wireguard.android.util.ExceptionLoggers;
@@ -47,13 +42,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 +59,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 +176,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 +269,54 @@ public class TunnelListFragment extends BaseFragment {
super.onViewStateRestored(savedInstanceState);
binding.setFragment(this);
binding.setTunnels(Application.getTunnelManager().getTunnels());
+ binding.setRowConfigurationHandler(new ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<Tunnel>() {
+ @Override
+ public void onConfigureRow(View view, Tunnel tunnel, int position) {
+ view.setOnClickListener(clicked -> {
+ if (actionMode == null) {
+ setSelectedTunnel(tunnel);
+ } else {
+ actionModeListener.toggleItemChecked(position);
+ }
+ });
+ view.setOnLongClickListener(clicked -> {
+ actionModeListener.toggleItemChecked(position);
+ return true;
+ });
+
+ view.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;
- }
- }
}
diff --git a/app/src/main/res/drawable/list_item_background_anim.xml b/app/src/main/res/drawable/list_item_background_anim.xml
index 98bfded9..213130c7 100644
--- a/app/src/main/res/drawable/list_item_background_anim.xml
+++ b/app/src/main/res/drawable/list_item_background_anim.xml
@@ -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>
diff --git a/app/src/main/res/layout/tunnel_list_fragment.xml b/app/src/main/res/layout/tunnel_list_fragment.xml
index 29b6fe08..cad2e094 100644
--- a/app/src/main/res/layout/tunnel_list_fragment.xml
+++ b/app/src/main/res/layout/tunnel_list_fragment.xml
@@ -11,6 +11,10 @@
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;" />
</data>
@@ -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"