Merge "Better event handling when coming from a trackpad." into arc-apps
am: 14b30ae40a

Change-Id: I47fe8abf63a05285e039e4b66178e1bcab639aa7
diff --git a/src/com/android/documentsui/base/Events.java b/src/com/android/documentsui/base/Events.java
index 3c57c81..dbadbfb 100644
--- a/src/com/android/documentsui/base/Events.java
+++ b/src/com/android/documentsui/base/Events.java
@@ -134,6 +134,20 @@
         /** Returns true if the action is the final release of a mouse or touch. */
         boolean isActionUp();
 
+        /**
+         * Returns true when the action is the initial press of a non-primary (ex. second finger)
+         * pointer.
+         * See {@link MotionEvent#ACTION_POINTER_DOWN}.
+         */
+        boolean isMultiPointerActionDown();
+
+        /**
+         * Returns true when the action is the final of a non-primary (ex. second finger)
+         * pointer.
+         * * See {@link MotionEvent#ACTION_POINTER_UP}.
+         */
+        boolean isMultiPointerActionUp();
+
         /** Returns true if the action is neither the initial nor the final release of a mouse
          * or touch. */
         boolean isActionMove();
@@ -150,6 +164,7 @@
         float getY();
         float getRawX();
         float getRawY();
+        int getPointerCount();
 
         /** Returns true if there is an item under the finger/cursor. */
         boolean isOverItem();
@@ -161,9 +176,17 @@
          */
         boolean isOverModelItem();
 
-        /** Returns true if the event is over an area that can be dragged via touch */
+        /**
+         * Returns true if the event is over an area that can be dragged via touch.
+         * List items have a white area that is not draggable.
+         */
         boolean isOverDragHotspot();
 
+        /**
+         * Returns true if the event is a two/three-finger scroll on touchpad.
+         */
+        boolean isTouchpadScroll();
+
         /** Returns the adapter position of the item under the finger/cursor. */
         int getItemPosition();
 
@@ -268,6 +291,17 @@
         }
 
         @Override
+        public boolean isMultiPointerActionDown() {
+            return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
+        }
+
+        @Override
+        public boolean isMultiPointerActionUp() {
+            return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
+        }
+
+
+        @Override
         public boolean isActionMove() {
             return mEvent.getActionMasked() == MotionEvent.ACTION_MOVE;
         }
@@ -303,6 +337,18 @@
         }
 
         @Override
+        public int getPointerCount() {
+            return mEvent.getPointerCount();
+        }
+
+        @Override
+        public boolean isTouchpadScroll() {
+            // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
+            // returned.
+            return isMouseEvent() && isActionMove() && mEvent.getButtonState() == 0;
+        }
+
+        @Override
         public boolean isOverDragHotspot() {
             return isOverItem() ? getDocumentDetails().isInDragHotspot(this) : false;
         }
@@ -357,6 +403,7 @@
                     .append(" isOverItem=").append(isOverItem())
                     .append(" getItemPosition=").append(getItemPosition())
                     .append(" getDocumentDetails=").append(getDocumentDetails())
+                    .append(" getPointerCount=").append(getPointerCount())
                     .append("}")
                     .toString();
         }
diff --git a/src/com/android/documentsui/dirlist/UserInputHandler.java b/src/com/android/documentsui/dirlist/UserInputHandler.java
index 8da25cf..e2a5d56 100644
--- a/src/com/android/documentsui/dirlist/UserInputHandler.java
+++ b/src/com/android/documentsui/dirlist/UserInputHandler.java
@@ -305,7 +305,8 @@
         // Don't scroll content window in response to mouse drag
         boolean onScroll(T event) {
             if (VERBOSE) Log.v(MTAG, "Delegated onScroll event.");
-            return true;
+            // If it's two-finger trackpad scrolling, we want to scroll
+            return !event.isTouchpadScroll();
         }
 
         boolean onSingleTapUp(T event) {
diff --git a/src/com/android/documentsui/selection/BandController.java b/src/com/android/documentsui/selection/BandController.java
index 5415faf..f429a8c 100644
--- a/src/com/android/documentsui/selection/BandController.java
+++ b/src/com/android/documentsui/selection/BandController.java
@@ -82,7 +82,8 @@
         this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, lock, gridItemTester);
     }
 
-    private BandController(
+    @VisibleForTesting
+    BandController(
             SelectionEnvironment env,
             DocumentsAdapter adapter,
             SelectionManager selectionManager,
@@ -174,7 +175,8 @@
         };
     }
 
-    private boolean isActive() {
+    @VisibleForTesting
+    boolean isActive() {
         return mModel != null;
     }
 
@@ -210,8 +212,8 @@
     }
 
     public boolean shouldStart(InputEvent e) {
-        // Don't start, or extend bands on right click.
-        if (e.isSecondaryButtonPressed()) {
+        // Don't start, or extend bands on non-left clicks.
+        if (!e.isPrimaryButtonPressed()) {
             return false;
         }
 
@@ -237,7 +239,7 @@
     public boolean shouldStop(InputEvent input) {
         return isActive()
                 && input.isMouseEvent()
-                && (input.isActionUp() || input.isActionCancel());
+                && (input.isActionUp() || input.isMultiPointerActionUp() || input.isActionCancel());
     }
 
     /**
diff --git a/tests/common/com/android/documentsui/testing/TestEvent.java b/tests/common/com/android/documentsui/testing/TestEvent.java
index 90261e9..391db15 100644
--- a/tests/common/com/android/documentsui/testing/TestEvent.java
+++ b/tests/common/com/android/documentsui/testing/TestEvent.java
@@ -73,6 +73,7 @@
 
     private @Action int mAction;
     private @ToolType int mToolType;
+    private int mPointerCount;
     private Set<Integer> mButtons;
     private Set<Integer> mKeys;
     private Point mLocation;
@@ -87,6 +88,7 @@
         mLocation = new Point(0, 0);
         mRawLocation = new Point(0, 0);
         mDetails = new Details();
+        mPointerCount = 0;
     }
 
     private TestEvent(TestEvent source) {
@@ -98,6 +100,7 @@
         mLocation = source.mLocation;
         mRawLocation = source.mRawLocation;
         mDetails = new Details(source.mDetails);
+        mPointerCount = source.mPointerCount;
     }
 
     @Override
@@ -126,6 +129,11 @@
     }
 
     @Override
+    public int getPointerCount() {
+        return mPointerCount;
+    }
+
+    @Override
     public boolean isMouseEvent() {
         return mToolType == MotionEvent.TOOL_TYPE_MOUSE;
     }
@@ -171,6 +179,16 @@
     }
 
     @Override
+    public boolean isMultiPointerActionDown() {
+        return mAction == MotionEvent.ACTION_POINTER_DOWN;
+    }
+
+    @Override
+    public boolean isMultiPointerActionUp() {
+        return mAction == MotionEvent.ACTION_POINTER_UP;
+    }
+
+    @Override
     public boolean isActionMove() {
         return mAction == MotionEvent.ACTION_MOVE;
     }
@@ -187,7 +205,7 @@
 
     @Override
     public boolean isOverDragHotspot() {
-        return mDetails.isOverInteractiveArea();
+        return isOverItem() && mDetails.isInDragHotspot(this);
     }
 
     @Override
@@ -200,6 +218,11 @@
     }
 
     @Override
+    public boolean isTouchpadScroll() {
+        return isMouseEvent() && mButtons.isEmpty() && isActionMove();
+    }
+
+    @Override
     public int getItemPosition() {
         return mDetails.mPosition;
     }
@@ -260,10 +283,6 @@
             return mPosition != Integer.MIN_VALUE && mPosition != RecyclerView.NO_POSITION;
         }
 
-        private boolean isOverInteractiveArea() {
-            return mPosition != Integer.MIN_VALUE && mPosition != RecyclerView.NO_POSITION;
-        }
-
         @Override
         public boolean hasModelId() {
             return !TextUtils.isEmpty(mModelId);
@@ -352,6 +371,11 @@
             return this;
         }
 
+        public Builder pointerCount(int count) {
+            mState.mPointerCount = count;
+            return this;
+        }
+
         /**
          * Adds one or more button press attributes.
          */
@@ -408,6 +432,11 @@
             return this;
         }
 
+        public Builder notInDragHotspot() {
+            mState.mDetails.mInDragHotspot = false;
+            return this;
+        }
+
         public Builder touch() {
             type(MotionEvent.TOOL_TYPE_FINGER);
             return this;
@@ -478,4 +507,4 @@
             return new TestEvent(mState);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
index b898653..76cff7c 100644
--- a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
@@ -90,6 +90,7 @@
                 .action(MotionEvent.ACTION_MOVE)
                 .mouse()
                 .at(1)
+                .inDragHotspot()
                 .primary();
     }
 
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
index bf27f81..6aff697 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
@@ -16,6 +16,7 @@
 
 package com.android.documentsui.dirlist;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.support.test.filters.SmallTest;
@@ -105,7 +106,12 @@
 
     @Test
     public void testScroll_shouldTrap() {
-        assertTrue(mInputHandler.onScroll(mEvent.at(0).build()));
+        assertTrue(mInputHandler.onScroll(mEvent.at(0).action(MotionEvent.ACTION_MOVE).primary().build()));
+    }
+
+    @Test
+    public void testScroll_NoTrapForTwoFinger() {
+        assertFalse(mInputHandler.onScroll(mEvent.at(0).action(MotionEvent.ACTION_MOVE).build()));
     }
 
     @Test
diff --git a/tests/unit/com/android/documentsui/selection/BandControllerTest.java b/tests/unit/com/android/documentsui/selection/BandControllerTest.java
new file mode 100644
index 0000000..fea021c
--- /dev/null
+++ b/tests/unit/com/android/documentsui/selection/BandControllerTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.selection;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.MotionEvent;
+
+import com.android.documentsui.DirectoryReloadLock;
+import com.android.documentsui.dirlist.TestData;
+import com.android.documentsui.dirlist.TestDocumentsAdapter;
+import com.android.documentsui.testing.SelectionManagers;
+import com.android.documentsui.testing.TestEvent.Builder;
+
+import java.util.Collections;
+import java.util.List;
+
+@SmallTest
+public class BandControllerTest extends AndroidTestCase {
+
+    private static final List<String> ITEMS = TestData.create(10);
+    private BandController mBandController;
+    private boolean mIsActive;
+
+    @Override
+    public void setUp() throws Exception {
+        mIsActive = false;
+        mBandController = new BandController(new TestSelectionEnvironment(),
+                new TestDocumentsAdapter(ITEMS), SelectionManagers.createTestInstance(ITEMS),
+                new DirectoryReloadLock(), null) {
+          @Override
+          public boolean isActive() {
+              return mIsActive;
+          }
+        };
+    }
+
+    public void testGoodStart() {
+        assertTrue(mBandController.shouldStart(goodStartEventBuilder().build()));
+    }
+
+    public void testBadStart_NoButtons() {
+        assertFalse(mBandController.shouldStart(
+                goodStartEventBuilder().releaseButton(MotionEvent.BUTTON_PRIMARY).build()));
+    }
+
+    public void testBadStart_SecondaryButton() {
+        assertFalse(
+                mBandController.shouldStart(goodStartEventBuilder().secondary().build()));
+    }
+
+    public void testBadStart_TertiaryButton() {
+        assertFalse(
+                mBandController.shouldStart(goodStartEventBuilder().tertiary().build()));
+    }
+
+    public void testBadStart_Touch() {
+        assertFalse(mBandController.shouldStart(
+                goodStartEventBuilder().touch().releaseButton(MotionEvent.BUTTON_PRIMARY).build()));
+    }
+
+    public void testBadStart_inDragSpot() {
+        assertFalse(
+                mBandController.shouldStart(goodStartEventBuilder().at(1).inDragHotspot().build()));
+    }
+
+    public void testBadStart_ActionDown() {
+        assertFalse(mBandController
+                .shouldStart(goodStartEventBuilder().action(MotionEvent.ACTION_DOWN).build()));
+    }
+
+    public void testBadStart_ActionUp() {
+        assertFalse(mBandController
+                .shouldStart(goodStartEventBuilder().action(MotionEvent.ACTION_UP).build()));
+    }
+
+    public void testBadStart_ActionPointerDown() {
+        assertFalse(mBandController.shouldStart(
+                goodStartEventBuilder().action(MotionEvent.ACTION_POINTER_DOWN).build()));
+    }
+
+    public void testBadStart_ActionPointerUp() {
+        assertFalse(mBandController.shouldStart(
+                goodStartEventBuilder().action(MotionEvent.ACTION_POINTER_UP).build()));
+    }
+
+    public void testBadStart_NoItems() {
+        mBandController = new BandController(new TestSelectionEnvironment(),
+                new TestDocumentsAdapter(Collections.EMPTY_LIST),
+                SelectionManagers.createTestInstance(ITEMS),
+                new DirectoryReloadLock(), null);
+        assertFalse(mBandController.shouldStart(goodStartEventBuilder().build()));
+    }
+
+    public void testBadStart_alreadyActive() {
+        mIsActive = true;
+        assertFalse(mBandController.shouldStart(goodStartEventBuilder().build()));
+    }
+
+    public void testGoodStop() {
+        mIsActive = true;
+        assertTrue(mBandController.shouldStop(goodStopEventBuilder().build()));
+    }
+
+    public void testGoodStop_PointerUp() {
+        mIsActive = true;
+        assertTrue(mBandController
+                .shouldStop(goodStopEventBuilder().action(MotionEvent.ACTION_POINTER_UP).build()));
+    }
+
+    public void testGoodStop_Cancel() {
+        mIsActive = true;
+        assertTrue(mBandController
+                .shouldStop(goodStopEventBuilder().action(MotionEvent.ACTION_CANCEL).build()));
+    }
+
+    public void testBadStop_NotActive() {
+        assertFalse(mBandController.shouldStop(goodStopEventBuilder().build()));
+    }
+
+    public void testBadStop_NonMouse() {
+        mIsActive = true;
+        assertFalse(mBandController.shouldStop(goodStopEventBuilder().touch().build()));
+    }
+
+    public void testBadStop_Move() {
+        mIsActive = true;
+        assertFalse(mBandController.shouldStop(
+                goodStopEventBuilder().action(MotionEvent.ACTION_MOVE).touch().build()));
+    }
+
+    public void testBadStop_Down() {
+        mIsActive = true;
+        assertFalse(mBandController.shouldStop(
+                goodStopEventBuilder().action(MotionEvent.ACTION_DOWN).touch().build()));
+    }
+
+
+    private Builder goodStartEventBuilder() {
+        return new Builder().mouse().primary().action(MotionEvent.ACTION_MOVE).notInDragHotspot();
+    }
+
+    private Builder goodStopEventBuilder() {
+        return new Builder().mouse().action(MotionEvent.ACTION_UP).notInDragHotspot();
+    }
+
+    private final class TestSelectionEnvironment implements BandController.SelectionEnvironment {
+        @Override
+        public void scrollBy(int dy) {
+        }
+
+        @Override
+        public void runAtNextFrame(Runnable r) {
+        }
+
+        @Override
+        public void removeCallback(Runnable r) {
+        }
+
+        @Override
+        public void showBand(Rect rect) {
+        }
+
+        @Override
+        public void hideBand() {
+        }
+
+        @Override
+        public void addOnScrollListener(OnScrollListener listener) {
+        }
+
+        @Override
+        public void removeOnScrollListener(OnScrollListener listener) {
+        }
+
+        @Override
+        public int getHeight() {
+            return 0;
+        }
+
+        @Override
+        public void invalidateView() {
+        }
+
+        @Override
+        public Point createAbsolutePoint(Point relativePoint) {
+            return null;
+        }
+
+        @Override
+        public Rect getAbsoluteRectForChildViewAt(int index) {
+            return null;
+        }
+
+        @Override
+        public int getAdapterPositionAt(int index) {
+            return 0;
+        }
+
+        @Override
+        public int getColumnCount() {
+            return 0;
+        }
+
+        @Override
+        public int getChildCount() {
+            return 0;
+        }
+
+        @Override
+        public int getVisibleChildCount() {
+            return 0;
+        }
+
+        @Override
+        public boolean hasView(int adapterPosition) {
+            return false;
+        }
+    }
+}