Let focused item also act as a starting anchor for range selection.
Bug: 31991343
Change-Id: Id59cc99a61550cd4a13feb508f13889a8598a3c9
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);