Clear selection when empty space is tapped.
Change-Id: I37f3ce8ec8d6b4c69a990ad129ff37229a8d2ac2
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 848ab93..8eb2ff6 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -1440,8 +1440,10 @@
}
void selectAllFiles() {
- mSelectionManager.selectItems(0, mAdapter.getItemCount());
- updateDisplayState();
+ boolean changed = mSelectionManager.setItemsSelected(0, mAdapter.getItemCount(), true);
+ if (changed) {
+ updateDisplayState();
+ }
}
private void setupDragAndDropOnDirectoryView(View view) {
diff --git a/src/com/android/documentsui/MultiSelectManager.java b/src/com/android/documentsui/MultiSelectManager.java
index 73def2d..a725dfd 100644
--- a/src/com/android/documentsui/MultiSelectManager.java
+++ b/src/com/android/documentsui/MultiSelectManager.java
@@ -16,6 +16,9 @@
package com.android.documentsui;
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.internal.util.Preconditions.checkState;
+
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
@@ -94,13 +97,16 @@
});
}
+ /**
+ * Constructs a new instance with {@code adapter} and {@code helper}.
+ * @param adapter
+ * @param helper
+ * @hide
+ */
+ @VisibleForTesting
MultiSelectManager(Adapter<?> adapter, RecyclerViewHelper helper) {
- if (adapter == null) {
- throw new IllegalArgumentException("Adapter cannot be null.");
- }
- if (helper == null) {
- throw new IllegalArgumentException("Helper cannot be null.");
- }
+ checkNotNull(adapter, "'adapter' cannot be null.");
+ checkNotNull(helper, "'helper' cannot be null.");
mHelper = helper;
mAdapter = adapter;
@@ -136,6 +142,12 @@
});
}
+ /**
+ * Adds {@code callback} such that it will be notified when {@code MultiSelectManager}
+ * events occur.
+ *
+ * @param callback
+ */
public void addCallback(MultiSelectManager.Callback callback) {
mCallbacks.add(callback);
}
@@ -165,18 +177,45 @@
return dest;
}
- public void selectItem(int position) {
- selectItems(position, 1);
- }
+ /**
+ * Causes item at {@code position} in adapter to be selected.
+ *
+ * @param position Adapter position
+ * @param selected
+ * @return True if the selection state of the item changed.
+ */
+ public boolean setItemSelected(int position, boolean selected) {
+ boolean changed = (selected)
+ ? mSelection.add(position)
+ : mSelection.remove(position);
- public void selectItems(int position, int length) {
- for (int i = position; i < position + length; i++) {
- mSelection.add(i);
+ if (changed) {
+ notifyItemStateChanged(position, true);
}
+ return changed;
}
+ /**
+ * @param position
+ * @param length
+ * @param selected
+ * @return True if the selection state of any of the items changed.
+ */
+ public boolean setItemsSelected(int position, int length, boolean selected) {
+ boolean changed = false;
+ for (int i = position; i < position + length; i++) {
+ changed |= setItemSelected(i, selected);
+ }
+ return changed;
+ }
+
+ /**
+ * Clears the selection.
+ */
public void clearSelection() {
- if (DEBUG) Log.d(TAG, "Clearing selection");
+ if (mSelection.isEmpty()) {
+ return;
+ }
if (mIntermediateSelection == null) {
mIntermediateSelection = new Selection();
}
@@ -185,14 +224,17 @@
for (int i = 0; i < mIntermediateSelection.size(); i++) {
int position = mIntermediateSelection.get(i);
- mAdapter.notifyItemChanged(position);
notifyItemStateChanged(position, false);
}
}
- public boolean onSingleTapUp(MotionEvent e) {
+ /**
+ * @param e
+ * @return true if the event was consumed.
+ */
+ private boolean onSingleTapUp(MotionEvent e) {
if (DEBUG) Log.d(TAG, "Handling tap event.");
- if (mSelection.size() == 0) {
+ if (mSelection.isEmpty()) {
return false;
}
@@ -200,25 +242,30 @@
}
/**
+ * TODO: Roll this into {@link #onSingleTapUp(MotionEvent)} once MotionEvent
+ * can be mocked.
+ *
* @param position
+ * @return true if the event was consumed.
* @hide
*/
@VisibleForTesting
boolean onSingleTapUp(int position) {
- if (mSelection.size() == 0) {
+ if (mSelection.isEmpty()) {
return false;
}
if (position == RecyclerView.NO_POSITION) {
- if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
- return false;
+ if (DEBUG) Log.d(TAG, "View is null. Canceling selection.");
+ clearSelection();
+ return true;
}
toggleSelection(position);
return true;
}
- public void onLongPress(MotionEvent e) {
+ private void onLongPress(MotionEvent e) {
if (DEBUG) Log.d(TAG, "Handling long press event.");
int position = mHelper.findEventPosition(e);
@@ -230,6 +277,9 @@
}
/**
+ * TODO: Roll this back into {@link #onLongPress(MotionEvent)} once MotionEvent
+ * can be mocked.
+ *
* @param position
* @hide
*/
@@ -255,7 +305,6 @@
if (notifyBeforeItemStateChange(position, nextState)) {
boolean selected = mSelection.flip(position);
notifyItemStateChanged(position, selected);
- mAdapter.notifyItemChanged(position);
if (DEBUG) Log.d(TAG, "Selection after long press: " + mSelection);
} else {
Log.i(TAG, "Selection change cancelled by listener.");
@@ -283,13 +332,13 @@
for (int i = lastListener; i > -1; i--) {
mCallbacks.get(i).onItemStateChanged(position, selected);
}
+ mAdapter.notifyItemChanged(position);
}
/**
- * Object representing the current selection.
+ * Object representing the current selection. Provides read only access
+ * public access, and private write access.
*/
- // NOTE: Much of the code in this class was copious swiped from
- // ArrayUtils, GrowingArrayUtils, and SparseBooleanArray.
public static final class Selection {
private SparseBooleanArray mSelection;
@@ -327,6 +376,13 @@
return mSelection.size();
}
+ /**
+ * @return true if the selection is empty.
+ */
+ public boolean isEmpty() {
+ return mSelection.size() == 0;
+ }
+
private boolean flip(int position) {
if (contains(position)) {
remove(position);
@@ -339,14 +395,22 @@
/** @hide */
@VisibleForTesting
- void add(int position) {
- mSelection.put(position, true);
+ boolean add(int position) {
+ if (!mSelection.get(position)) {
+ mSelection.put(position, true);
+ return true;
+ }
+ return false;
}
/** @hide */
@VisibleForTesting
- void remove(int position) {
- mSelection.delete(position);
+ boolean remove(int position) {
+ if (mSelection.get(position)) {
+ mSelection.delete(position);
+ return true;
+ }
+ return false;
}
/**
@@ -360,12 +424,8 @@
*/
@VisibleForTesting
void expand(int startPosition, int count) {
- if (startPosition < 0) {
- throw new IllegalArgumentException("startPosition must be non-negative");
- }
- if (count < 1) {
- throw new IllegalArgumentException("countMust be greater than 0");
- }
+ checkState(startPosition >= 0);
+ checkState(count > 0);
for (int i = 0; i < mSelection.size(); i++) {
int itemPosition = mSelection.keyAt(i);
@@ -386,12 +446,8 @@
*/
@VisibleForTesting
void collapse(int startPosition, int count) {
- if (startPosition < 0) {
- throw new IllegalArgumentException("startPosition must be non-negative");
- }
- if (count < 1) {
- throw new IllegalArgumentException("countMust be greater than 0");
- }
+ checkState(startPosition >= 0);
+ checkState(count > 0);
int endPosition = startPosition + count - 1;
@@ -481,7 +537,7 @@
/**
* A composite {@code OnGestureDetector} that allows us to delegate unhandled
- * events to other interested parties.
+ * events to an outside party (presumably DirectoryFragment).
*/
private static final class CompositeOnGestureListener implements OnGestureListener {
diff --git a/tests/src/com/android/documentsui/MultiSelectManagerTest.java b/tests/src/com/android/documentsui/MultiSelectManagerTest.java
index 57677d3..20fb07e 100644
--- a/tests/src/com/android/documentsui/MultiSelectManagerTest.java
+++ b/tests/src/com/android/documentsui/MultiSelectManagerTest.java
@@ -64,33 +64,41 @@
}
@Test
- public void singleTapDoesNotSelectBeforeLongPress() {
- mManager.onSingleTapUp(99);
- assertSelection();
- }
-
- @Test
- public void longPressStartsSelectionMode() {
+ public void longPress_StartsSelectionMode() {
mManager.onLongPress(7);
assertSelection(7);
}
@Test
- public void secondLongPressExtendsSelection() {
+ public void longPress_SecondPressExtendsSelection() {
mManager.onLongPress(7);
mManager.onLongPress(99);
assertSelection(7, 99);
}
@Test
- public void singleTapUnselectedLastItem() {
+ public void singleTapUp_DoesNotSelectBeforeLongPress() {
+ mManager.onSingleTapUp(99);
+ assertSelection();
+ }
+
+ @Test
+ public void singleTapUp_UnselectsSelectedItem() {
mManager.onLongPress(7);
mManager.onSingleTapUp(7);
assertSelection();
}
@Test
- public void singleTapUpExtendsSelection() {
+ public void singleTapUp_NoPositionClearsSelection() {
+ mManager.onLongPress(7);
+ mManager.onSingleTapUp(11);
+ mManager.onSingleTapUp(RecyclerView.NO_POSITION);
+ assertSelection();
+ }
+
+ @Test
+ public void singleTapUp_ExtendsSelection() {
mManager.onLongPress(99);
mManager.onSingleTapUp(7);
mManager.onSingleTapUp(13);
diff --git a/tests/src/com/android/documentsui/MultiSelectManager_SelectionTest.java b/tests/src/com/android/documentsui/MultiSelectManager_SelectionTest.java
index 58f2e09..51b542b 100644
--- a/tests/src/com/android/documentsui/MultiSelectManager_SelectionTest.java
+++ b/tests/src/com/android/documentsui/MultiSelectManager_SelectionTest.java
@@ -59,6 +59,13 @@
}
@Test
+ public void isEmpty() {
+ assertTrue(new Selection().isEmpty());
+ selection.clear();
+ assertTrue(selection.isEmpty());
+ }
+
+ @Test
public void sizeAndGet() {
Selection other = new Selection();
for (int i = 0; i < selection.size(); i++) {