Merge "Let focused item also act as a starting anchor for range selection." into nyc-andromeda-dev
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index e9360ab..ea029d9 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -92,7 +92,6 @@
     protected MessageBuilder mMessages;
     protected DrawerController mDrawer;
     protected NavigationViewManager mNavigator;
-    protected FocusManager mFocusManager;
     protected SortController mSortController;
 
     protected T mActions;
@@ -165,10 +164,8 @@
     public abstract ActionModeController getActionModeController(
             SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view);
 
-    public final FocusManager getFocusManager(RecyclerView view, Model model) {
-        assert(mFocusManager != null);
-        return mFocusManager.reset(view, model);
-    }
+
+    public abstract FocusManager getFocusManager(RecyclerView view, Model model);
 
     public final MessageBuilder getMessages() {
         assert(mMessages != null);
@@ -190,7 +187,6 @@
         setContentView(mLayoutId);
 
         mState = getState(icicle);
-        mFocusManager = new FocusManager(getColor(R.color.accent_dark));
         mDrawer = DrawerController.create(this, getActivityConfig());
         Metrics.logActivityLaunch(this, mState, intent);
 
diff --git a/src/com/android/documentsui/FocusManager.java b/src/com/android/documentsui/FocusManager.java
index 8f5d388..965dc55 100644
--- a/src/com/android/documentsui/FocusManager.java
+++ b/src/com/android/documentsui/FocusManager.java
@@ -17,6 +17,7 @@
 package com.android.documentsui;
 
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
+import static com.android.documentsui.base.Shared.DEBUG;
 
 import android.annotation.ColorRes;
 import android.annotation.Nullable;
@@ -45,6 +46,7 @@
 import com.android.documentsui.dirlist.FocusHandler;
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.selection.SelectionManager;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -59,8 +61,10 @@
 
     private final ContentScope mScope = new ContentScope();
     private final TitleSearchHelper mSearchHelper;
+    private final SelectionManager mSelectionMgr;
 
-    public FocusManager(@ColorRes int color) {
+    public FocusManager(@ColorRes int color, SelectionManager selectionMgr) {
+        mSelectionMgr = selectionMgr;
         mSearchHelper = new TitleSearchHelper(color);
     }
 
@@ -94,19 +98,24 @@
     }
 
     @Override
-    public void restoreLastFocus() {
+    public boolean requestFocus() {
         if (mScope.adapter.getItemCount() == 0) {
-            // Nothing to focus.
-            return;
+            if (DEBUG) Log.v(TAG, "Nothing to focus.");
+            return false;
         }
 
-        if (mScope.lastFocusPosition != RecyclerView.NO_POSITION) {
-            // The system takes care of situations when a view is no longer on screen, etc,
-            focusItem(mScope.lastFocusPosition);
-        } else {
-            // Focus the first visible item
-            focusItem(mScope.layout.findFirstVisibleItemPosition());
+        // If there's a selection going on, we don't want to grant user the ability to focus
+        // on any individual item to prevent ambiguity in operations (Cut selection vs. Cut focused
+        // item)
+        if (mSelectionMgr.hasSelection()) {
+            if (DEBUG) Log.v(TAG, "Existing selection found. No focus will be done.");
+            return false;
         }
+
+        final int focusPos = (mScope.lastFocusPosition != RecyclerView.NO_POSITION)
+                ? mScope.lastFocusPosition : mScope.layout.findFirstVisibleItemPosition();
+        focusItem(focusPos);
+        return true;
     }
 
     /*
@@ -147,6 +156,11 @@
     }
 
     @Override
+    public boolean hasFocusedItem() {
+        return mScope.lastFocusPosition != RecyclerView.NO_POSITION;
+    }
+
+    @Override
     public @Nullable String getFocusModelId() {
         if (mScope.lastFocusPosition != RecyclerView.NO_POSITION) {
             DocumentHolder holder = (DocumentHolder) mScope.view
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 59b58f1..efbbeed 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -885,11 +885,7 @@
      * Attempts to restore focus on the directory listing.
      */
     public boolean requestFocus() {
-        if (mSelectionMgr.hasSelection()) {
-            return false;
-        }
-        mFocusManager.restoreLastFocus();
-        return true;
+        return mFocusManager.requestFocus();
     }
 
     private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
diff --git a/src/com/android/documentsui/dirlist/FocusHandler.java b/src/com/android/documentsui/dirlist/FocusHandler.java
index 1cbb8a9..8db8ffb 100644
--- a/src/com/android/documentsui/dirlist/FocusHandler.java
+++ b/src/com/android/documentsui/dirlist/FocusHandler.java
@@ -44,9 +44,10 @@
     void focusDocument(String modelId);
 
     /**
-     * Requests focus on the item that last had focus. Scrolls to that item if necessary.
+     * Requests focus on the item that last had focus. Scrolls to that item if necessary. If focus
+     * is unsuccessful, return false.
      */
-    void restoreLastFocus();
+    boolean requestFocus();
 
     /**
      * @return The adapter position of the last focused item.
@@ -54,6 +55,11 @@
     int getFocusPosition();
 
     /**
+     * @return True if there is currently an item in focus, false otherwise.
+     */
+    boolean hasFocusedItem();
+
+    /**
      * @return The modelId of the last focused item. If no item is focused, this should return null.
      */
     @Nullable String getFocusModelId();
diff --git a/src/com/android/documentsui/dirlist/UserInputHandler.java b/src/com/android/documentsui/dirlist/UserInputHandler.java
index 1e9cc39..e107c2b 100644
--- a/src/com/android/documentsui/dirlist/UserInputHandler.java
+++ b/src/com/android/documentsui/dirlist/UserInputHandler.java
@@ -375,7 +375,13 @@
                 return false;
             }
 
-            return selectDocument(doc);
+            if (mFocusHandler.hasFocusedItem() && event.isShiftKeyDown()) {
+                mSelectionMgr.formNewSelectionRange(mFocusHandler.getFocusPosition(),
+                        doc.getAdapterPosition());
+                return true;
+            } else {
+                return selectDocument(doc);
+            }
         }
 
         boolean onDoubleTap(T event) {
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 0c67b20..8c6ca5d 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -26,6 +26,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.CallSuper;
+import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
@@ -38,6 +39,7 @@
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.DragShadowBuilder;
+import com.android.documentsui.FocusManager;
 import com.android.documentsui.MenuManager.DirectoryDetails;
 import com.android.documentsui.MenuManager.SelectionDetails;
 import com.android.documentsui.OperationDialogFragment;
@@ -56,7 +58,6 @@
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.DocumentsAdapter;
 import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.selection.Selection;
 import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
 import com.android.documentsui.services.FileOperationService;
@@ -81,6 +82,7 @@
     private ScopedPreferences mPrefs;
     private SelectionManager mSelectionMgr;
     private MenuManager mMenuManager;
+    private FocusManager mFocusManager;
     private DialogController mDialogs;
     private DocumentClipper mClipper;
     private ActionModeController mActionModeController;
@@ -102,6 +104,7 @@
 
         mClipper = DocumentsApplication.getDocumentClipper(this);
         mSelectionMgr = new SelectionManager(SelectionManager.MODE_MULTIPLE);
+        mFocusManager = new FocusManager(getColor(R.color.accent_dark), mSelectionMgr);
         mMenuManager = new MenuManager(
                 mSearchManager,
                 mState,
@@ -388,6 +391,12 @@
     }
 
     @Override
+    public FocusManager getFocusManager(RecyclerView view, Model model) {
+        assert (mFocusManager != null);
+        return mFocusManager.reset(view, model);
+    }
+
+    @Override
     public final ActionModeController getActionModeController(
             SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view) {
         return mActionModeController.reset(selectionDetails, menuItemClicker, view);
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index eee92e5..3e9d6aa 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -38,6 +38,7 @@
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.support.design.widget.Snackbar;
+import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -47,6 +48,7 @@
 import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.FocusManager;
 import com.android.documentsui.MenuManager.DirectoryDetails;
 import com.android.documentsui.MenuManager.SelectionDetails;
 import com.android.documentsui.ProviderExecutor;
@@ -85,6 +87,7 @@
     private ScopedPreferences mPrefs;
     private SelectionManager mSelectionMgr;
     private MenuManager mMenuManager;
+    private FocusManager mFocusManager;
     private ActionModeController mActionModeController;
 
     public PickActivity() {
@@ -104,6 +107,7 @@
                 mState.allowMultiple
                         ? SelectionManager.MODE_MULTIPLE
                         : SelectionManager.MODE_SINGLE);
+        mFocusManager = new FocusManager(getColor(R.color.accent_dark), mSelectionMgr);
         mMenuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this));
         mActions = new ActionHandler<>(
                 this,
@@ -441,6 +445,12 @@
     }
 
     @Override
+    public FocusManager getFocusManager(RecyclerView view, Model model) {
+        assert (mFocusManager != null);
+        return mFocusManager.reset(view, model);
+    }
+
+    @Override
     public ActionModeController getActionModeController(
             SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view) {
         return mActionModeController.reset(selectionDetails, menuItemClicker, view);
diff --git a/src/com/android/documentsui/selection/SelectionManager.java b/src/com/android/documentsui/selection/SelectionManager.java
index 103ef8f..d6fcaef 100644
--- a/src/com/android/documentsui/selection/SelectionManager.java
+++ b/src/com/android/documentsui/selection/SelectionManager.java
@@ -298,6 +298,16 @@
         snapRangeSelection(pos, RANGE_PROVISIONAL);
     }
 
+    /*
+     * Starts and extends range selection in one go. This assumes item at startPos is not selected
+     * beforehand.
+     */
+    public void formNewSelectionRange(int startPos, int endPos) {
+        assert(!mSelection.contains(mAdapter.getModelId(startPos)));
+        startRangeSelection(startPos);
+        snapRangeSelection(endPos);
+    }
+
     /**
      * Sets the end point for the current range selection, started by a call to
      * {@link #startRangeSelection(int)}. This function should only be called when a range selection
diff --git a/tests/common/com/android/documentsui/dirlist/TestFocusHandler.java b/tests/common/com/android/documentsui/dirlist/TestFocusHandler.java
index 76db4e1..3cbe1f1 100644
--- a/tests/common/com/android/documentsui/dirlist/TestFocusHandler.java
+++ b/tests/common/com/android/documentsui/dirlist/TestFocusHandler.java
@@ -25,6 +25,8 @@
 public final class TestFocusHandler implements FocusHandler {
 
     public boolean handleKey;
+    public int focusPos = 0;
+    public String focusModelId;
 
     @Override
     public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
@@ -36,17 +38,23 @@
     }
 
     @Override
-    public void restoreLastFocus() {
+    public boolean requestFocus() {
+        return true;
+    }
+
+    @Override
+    public boolean hasFocusedItem() {
+        return true;
     }
 
     @Override
     public int getFocusPosition() {
-        return 0;
+        return focusPos;
     }
 
     @Override
     public String getFocusModelId() {
-        return null;
+        return focusModelId;
     }
 
     @Override
diff --git a/tests/unit/com/android/documentsui/FocusManagerTest.java b/tests/unit/com/android/documentsui/FocusManagerTest.java
index 898f479..0f38353 100644
--- a/tests/unit/com/android/documentsui/FocusManagerTest.java
+++ b/tests/unit/com/android/documentsui/FocusManagerTest.java
@@ -21,8 +21,11 @@
 
 import com.android.documentsui.dirlist.TestData;
 import com.android.documentsui.dirlist.TestModel;
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.TestRecyclerView;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @SmallTest
@@ -34,11 +37,14 @@
 
     private FocusManager mManager;
     private TestRecyclerView mView;
+    private SelectionManager mSelectionMgr;
 
     @Override
     public void setUp() throws Exception {
         mView = TestRecyclerView.create(ITEMS);
-        mManager = new FocusManager(0).reset(mView, new TestModel(TEST_AUTHORITY));
+        mSelectionMgr = SelectionManagers.createTestInstance(ITEMS);
+        mManager = new FocusManager(0, mSelectionMgr).reset(mView,
+                new TestModel(TEST_AUTHORITY));
     }
 
     public void testFocus() {
@@ -54,4 +60,16 @@
        // Should only be called once
        mView.assertItemViewFocused(10);
     }
+
+    public void testRequestFocus_noItemsToFocus() {
+        mView = TestRecyclerView.create(new ArrayList<>());
+        mManager = new FocusManager(0, SelectionManagers.createTestInstance()).reset(mView,
+                new TestModel(TEST_AUTHORITY));
+        assertFalse(mManager.requestFocus());
+    }
+
+    public void testRequestFocus_hasSelection() {
+        mSelectionMgr.toggleSelection("0");
+        assertFalse(mManager.requestFocus());
+    }
 }
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
index 41ed786..bf27f81 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
@@ -47,7 +47,9 @@
 
     private UserInputHandler<TestEvent> mInputHandler;
     private TestActionHandler mActionHandler;
+    private TestFocusHandler mFocusHandler;
     private SelectionProbe mSelection;
+    private SelectionManager mSelectionMgr;
     private TestPredicate<DocumentDetails> mCanSelect;
     private TestEventHandler<InputEvent> mContextMenuClickHandler;
     private TestEventHandler<InputEvent> mDragAndDropHandler;
@@ -58,19 +60,20 @@
     @Before
     public void setUp() {
 
-        SelectionManager selectionMgr = SelectionManagers.createTestInstance(ITEMS);
+        mSelectionMgr = SelectionManagers.createTestInstance(ITEMS);
         mActionHandler = new TestActionHandler();
 
-        mSelection = new SelectionProbe(selectionMgr);
+        mSelection = new SelectionProbe(mSelectionMgr);
         mCanSelect = new TestPredicate<>();
         mContextMenuClickHandler = new TestEventHandler<>();
         mDragAndDropHandler = new TestEventHandler<>();
         mGestureSelectHandler = new TestEventHandler<>();
+        mFocusHandler = new TestFocusHandler();
 
         mInputHandler = new UserInputHandler<>(
                 mActionHandler,
-                new TestFocusHandler(),
-                selectionMgr,
+                mFocusHandler,
+                mSelectionMgr,
                 (MotionEvent event) -> {
                     throw new UnsupportedOperationException("Not exercised in tests.");
                 },
@@ -130,6 +133,18 @@
     }
 
     @Test
+    public void testConfirmedShiftClick_ExtendsSelectionFromOriginFocus() {
+        mFocusHandler.focusPos = 7;
+        mFocusHandler.focusModelId = "7";
+        // This is a hack-y test, since the real FocusManager would've set range begin itself.
+        mSelectionMgr.setSelectionRangeBegin(7);
+        mSelection.assertNoSelection();
+
+        mInputHandler.onSingleTapConfirmed(mEvent.at(11).shift().build());
+        mSelection.assertSelection(7, 8, 9, 10, 11);
+    }
+
+    @Test
     public void testUnconfirmedShiftClick_RotatesAroundOrigin() {
         mInputHandler.onSingleTapConfirmed(mEvent.at(7).build());
 
diff --git a/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java b/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java
index d25395a..3e1e7ed 100644
--- a/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java
+++ b/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java
@@ -202,7 +202,6 @@
         mManager.snapProvisionalRangeSelection(18);
         mSelection.assertRangeSelected(11, 18);
         mManager.endRangeSelection();
-
         mSelection.assertRangeSelected(13, 15);
         mSelection.assertRangeSelected(11, 11);
         mSelection.assertSelectionSize(4);