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++) {