Merge "Address minor comments after go/ag/858723" into nyc-dev
diff --git a/packages/DocumentsUI/res/layout/fixed_layout.xml b/packages/DocumentsUI/res/layout/fixed_layout.xml
index 8414feb..84a928d 100644
--- a/packages/DocumentsUI/res/layout/fixed_layout.xml
+++ b/packages/DocumentsUI/res/layout/fixed_layout.xml
@@ -16,11 +16,14 @@
<!-- CoordinatorLayout is necessary for various components (e.g. Snackbars, and
floating action buttons) to operate correctly. -->
+<!-- focusableInTouchMode is set in order to force key events to go to the activity's global key
+ callback, which is necessary for proper event routing. See BaseActivity.onKeyDown. -->
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:id="@+id/coordinator_layout">
+ android:id="@+id/coordinator_layout"
+ android:focusableInTouchMode="true">
<LinearLayout
android:layout_width="match_parent"
diff --git a/packages/DocumentsUI/res/layout/fragment_directory.xml b/packages/DocumentsUI/res/layout/fragment_directory.xml
index d0364ff..0fb74e5 100644
--- a/packages/DocumentsUI/res/layout/fragment_directory.xml
+++ b/packages/DocumentsUI/res/layout/fragment_directory.xml
@@ -83,7 +83,7 @@
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
- android:id="@+id/list"
+ android:id="@+id/dir_list"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/packages/DocumentsUI/res/layout/fragment_roots.xml b/packages/DocumentsUI/res/layout/fragment_roots.xml
index f3de3b4..b33b8d0 100644
--- a/packages/DocumentsUI/res/layout/fragment_roots.xml
+++ b/packages/DocumentsUI/res/layout/fragment_roots.xml
@@ -14,8 +14,8 @@
limitations under the License.
-->
-<ListView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@android:id/list"
+<com.android.documentsui.RootsList xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/roots_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
diff --git a/packages/DocumentsUI/res/layout/single_pane_layout.xml b/packages/DocumentsUI/res/layout/single_pane_layout.xml
index f53d698..235d22d 100644
--- a/packages/DocumentsUI/res/layout/single_pane_layout.xml
+++ b/packages/DocumentsUI/res/layout/single_pane_layout.xml
@@ -16,11 +16,14 @@
<!-- CoordinatorLayout is necessary for various components (e.g. Snackbars, and
floating action buttons) to operate correctly. -->
+<!-- focusableInTouchMode is set in order to force key events to go to the activity's global key
+ callback, which is necessary for proper event routing. See BaseActivity.onKeyDown. -->
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:id="@+id/coordinator_layout">
+ android:id="@+id/coordinator_layout"
+ android:focusableInTouchMode="true">
<LinearLayout
android:layout_width="match_parent"
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index 475387b..4a55906 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -42,6 +42,7 @@
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.util.Log;
+import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Spinner;
@@ -83,6 +84,8 @@
// We use the time gap to figure out whether to close app or reopen the drawer.
private long mDrawerLastFiddled;
+ private boolean mNavDrawerHasFocus;
+
public abstract void onDocumentPicked(DocumentInfo doc, @Nullable SiblingProvider siblings);
public abstract void onDocumentsPicked(List<DocumentInfo> docs);
@@ -580,6 +583,54 @@
}
}
+ /**
+ * Declare a global key handler to route key events when there isn't a specific focus view. This
+ * covers the scenario where a user opens DocumentsUI and just starts typing.
+ *
+ * @param keyCode
+ * @param event
+ * @return
+ */
+ @CallSuper
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (Events.isNavigationKeyCode(keyCode)) {
+ // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any
+ // stray navigation keystrokes focus the content pane, which is probably what the user
+ // is trying to do.
+ DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
+ if (df != null) {
+ df.requestFocus();
+ return true;
+ }
+ } else if (keyCode == KeyEvent.KEYCODE_TAB) {
+ toggleNavDrawerFocus();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ /**
+ * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't
+ * locked, open/close it as appropriate.
+ */
+ void toggleNavDrawerFocus() {
+ if (mNavDrawerHasFocus) {
+ mDrawer.setOpen(false);
+ DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
+ if (df != null) {
+ df.requestFocus();
+ }
+ } else {
+ mDrawer.setOpen(true);
+ RootsFragment rf = RootsFragment.get(getFragmentManager());
+ if (rf != null) {
+ rf.requestFocus();
+ }
+ }
+ mNavDrawerHasFocus = !mNavDrawerHasFocus;
+ }
+
DocumentInfo getRootDocumentBlocking(RootInfo root) {
try {
final Uri uri = DocumentsContract.buildDocumentUri(
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
index 7dac0c1..0e27622 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
@@ -83,7 +83,7 @@
final View view = inflater.inflate(R.layout.fragment_directory, container, false);
- mRecView = (RecyclerView) view.findViewById(R.id.list);
+ mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
mRecView.setLayoutManager(new LinearLayoutManager(getContext()));
mRecView.addOnItemTouchListener(mItemListener);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index 26bda312..53f8297 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -89,7 +89,7 @@
final Context context = inflater.getContext();
final View view = inflater.inflate(R.layout.fragment_roots, container, false);
- mList = (ListView) view.findViewById(android.R.id.list);
+ mList = (ListView) view.findViewById(R.id.roots_list);
mList.setOnItemClickListener(mItemListener);
mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
return view;
@@ -167,6 +167,13 @@
}
}
+ /**
+ * Attempts to shift focus back to the navigation drawer.
+ */
+ public void requestFocus() {
+ mList.requestFocus();
+ }
+
private void showAppDetails(ResolveInfo ri) {
final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", ri.activityInfo.packageName, null));
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsList.java b/packages/DocumentsUI/src/com/android/documentsui/RootsList.java
new file mode 100644
index 0000000..bf03ffd
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsList.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.ListView;
+
+/**
+ * The list in the navigation drawer. This class exists for the purpose of overriding the key
+ * handler on ListView. Ignoring keystrokes (e.g. the tab key) cannot be properly done using
+ * View.OnKeyListener.
+ */
+public class RootsList extends ListView {
+
+ // Multiple constructors are needed to handle all the different ways this View could be
+ // constructed by the framework. Don't remove them!
+ public RootsList(Context context) {
+ super(context);
+ }
+
+ public RootsList(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public RootsList(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public RootsList(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ // Ignore tab key events - this causes them to bubble up to the global key handler where
+ // they are appropriately handled. See BaseActivity.onKeyDown.
+ case KeyEvent.KEYCODE_TAB:
+ return false;
+ // Prevent left/right arrow keystrokes from shifting focus away from the roots list.
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ return true;
+ default:
+ return super.onKeyDown(keyCode, event);
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 174984c..726538e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -183,7 +183,7 @@
mEmptyView = view.findViewById(android.R.id.empty);
- mRecView = (RecyclerView) view.findViewById(R.id.list);
+ mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
mRecView.setRecyclerListener(
new RecyclerListener() {
@Override
@@ -263,6 +263,7 @@
mSelectionManager.addCallback(selectionListener);
+ // Make sure this is done after the RecyclerView is set up.
mFocusManager = new FocusManager(mRecView, mSelectionManager);
mModel = new Model();
@@ -834,6 +835,7 @@
@Override
public void initDocumentHolder(DocumentHolder holder) {
holder.addEventListener(mItemEventListener);
+ holder.itemView.setOnFocusChangeListener(mFocusManager);
}
@Override
@@ -1054,6 +1056,13 @@
}
}
+ /**
+ * Attempts to restore focus on the directory listing.
+ */
+ public void requestFocus() {
+ mFocusManager.restoreLastFocus();
+ }
+
private void setupDragAndDropOnDirectoryView(View view) {
// Listen for drops on non-directory items and empty space.
view.setOnDragListener(mOnDragListener);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
index 86b9146..ad010a6 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
@@ -27,7 +27,7 @@
/**
* A class that handles navigation and focus within the DirectoryFragment.
*/
-class FocusManager {
+class FocusManager implements View.OnFocusChangeListener {
private static final String TAG = "FocusManager";
private RecyclerView mView;
@@ -35,6 +35,8 @@
private LinearLayoutManager mLayout;
private MultiSelectManager mSelectionManager;
+ private int mLastFocusPosition = RecyclerView.NO_POSITION;
+
public FocusManager(RecyclerView view, MultiSelectManager selectionManager) {
mView = view;
mAdapter = view.getAdapter();
@@ -52,24 +54,46 @@
* @return Whether the event was handled.
*/
public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
- boolean handled = false;
if (Events.isNavigationKeyCode(keyCode)) {
// Find the target item and focus it.
int endPos = findTargetPosition(doc.itemView, keyCode, event);
if (endPos != RecyclerView.NO_POSITION) {
focusItem(endPos);
+ boolean extendSelection = event.isShiftPressed();
// Handle any necessary adjustments to selection.
- boolean extendSelection = event.isShiftPressed();
if (extendSelection) {
int startPos = doc.getAdapterPosition();
mSelectionManager.selectRange(startPos, endPos);
}
- handled = true;
}
+ // Swallow all navigation keystrokes. Otherwise they go to the app's global
+ // key-handler, which will route them back to the DF and cause focus to be reset.
+ return true;
}
- return handled;
+ return false;
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ // Remember focus events on items.
+ if (hasFocus && v.getParent() == mView) {
+ mLastFocusPosition = mView.getChildAdapterPosition(v);
+ }
+ }
+
+ /**
+ * Requests focus on the item that last had focus. Scrolls to that item if necessary.
+ */
+ public void restoreLastFocus() {
+ if (mLastFocusPosition != RecyclerView.NO_POSITION) {
+ // The system takes care of situations when a view is no longer on screen, etc,
+ focusItem(mLastFocusPosition);
+ } else {
+ // Focus the first visible item
+ focusItem(mLayout.findFirstVisibleItemPosition());
+ }
}
/**
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
index 77f16d9..609dc0c 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
@@ -21,6 +21,7 @@
import android.os.RemoteException;
import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
@LargeTest
public class FilesActivityUiTest extends ActivityTest<FilesActivity> {
@@ -115,4 +116,37 @@
bot.waitForDeleteSnackbarGone();
assertFalse(bot.hasDocuments("poodles.text"));
}
+
+ // Tests that pressing tab switches focus between the roots and directory listings.
+ public void testKeyboard_tab() throws Exception {
+ bot.pressKey(KeyEvent.KEYCODE_TAB);
+ bot.assertHasFocus("com.android.documentsui:id/roots_list");
+ bot.pressKey(KeyEvent.KEYCODE_TAB);
+ bot.assertHasFocus("com.android.documentsui:id/dir_list");
+ }
+
+ // Tests that arrow keys do not switch focus away from the dir list.
+ public void testKeyboard_arrowsDirList() throws Exception {
+ for (int i = 0; i < 10; i++) {
+ bot.pressKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ bot.assertHasFocus("com.android.documentsui:id/dir_list");
+ }
+ for (int i = 0; i < 10; i++) {
+ bot.pressKey(KeyEvent.KEYCODE_DPAD_RIGHT);
+ bot.assertHasFocus("com.android.documentsui:id/dir_list");
+ }
+ }
+
+ // Tests that arrow keys do not switch focus away from the roots list.
+ public void testKeyboard_arrowsRootsList() throws Exception {
+ bot.pressKey(KeyEvent.KEYCODE_TAB);
+ for (int i = 0; i < 10; i++) {
+ bot.pressKey(KeyEvent.KEYCODE_DPAD_RIGHT);
+ bot.assertHasFocus("com.android.documentsui:id/roots_list");
+ }
+ for (int i = 0; i < 10; i++) {
+ bot.pressKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ bot.assertHasFocus("com.android.documentsui:id/roots_list");
+ }
+ }
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
index 4534c40..d2f8403 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
@@ -71,7 +71,7 @@
UiObject findRoot(String label) throws UiObjectNotFoundException {
final UiSelector rootsList = new UiSelector().resourceId(
"com.android.documentsui:id/container_roots").childSelector(
- new UiSelector().resourceId("android:id/list"));
+ new UiSelector().resourceId("com.android.documentsui:id/roots_list"));
// We might need to expand drawer if not visible
if (!new UiObject(rootsList).waitForExists(mTimeout)) {
@@ -195,6 +195,15 @@
assertNotNull(getSnackbar(mContext.getString(id)));
}
+ /**
+ * Asserts that the specified view or one of its descendents has focus.
+ */
+ void assertHasFocus(String resourceName) {
+ UiObject2 candidate = mDevice.findObject(By.res(resourceName));
+ assertNotNull("Expected " + resourceName + " to have focus, but it didn't.",
+ candidate.findObject(By.focused(true)));
+ }
+
void openDocument(String label) throws UiObjectNotFoundException {
int toolType = Configurator.getInstance().getToolType();
Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_FINGER);
@@ -309,7 +318,7 @@
UiObject findDocument(String label) throws UiObjectNotFoundException {
final UiSelector docList = new UiSelector().resourceId(
"com.android.documentsui:id/container_directory").childSelector(
- new UiSelector().resourceId("com.android.documentsui:id/list"));
+ new UiSelector().resourceId("com.android.documentsui:id/dir_list"));
// Wait for the first list item to appear
new UiObject(docList.childSelector(new UiSelector())).waitForExists(mTimeout);
@@ -330,7 +339,7 @@
UiObject findDocumentsList() {
return findObject(
"com.android.documentsui:id/container_directory",
- "com.android.documentsui:id/list");
+ "com.android.documentsui:id/dir_list");
}
UiObject findSearchView() {
@@ -416,4 +425,8 @@
mDevice.wait(Until.hasObject(By.pkg(TARGET_PKG).depth(0)), mTimeout);
mDevice.waitForIdle();
}
+
+ void pressKey(int keyCode) {
+ mDevice.pressKeyCode(keyCode);
+ }
}