FocusManager.java revision 047182631669608af946480c2545a10acb2ef1bf
115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa/* 215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Copyright (C) 2016 The Android Open Source Project 315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Licensed under the Apache License, Version 2.0 (the "License"); 515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * you may not use this file except in compliance with the License. 615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * You may obtain a copy of the License at 715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * http://www.apache.org/licenses/LICENSE-2.0 915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 1015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Unless required by applicable law or agreed to in writing, software 1115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * distributed under the License is distributed on an "AS IS" BASIS, 1215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * See the License for the specific language governing permissions and 1415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * limitations under the License. 1515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 1615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 1717b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKaypackage com.android.documentsui; 1815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 19d080506e3aa8547605cd4783eb660775d7d2b8eeSteve McKayimport static com.android.documentsui.base.DocumentInfo.getCursorString; 2075b7b9039cf0efcb188e916c6f510328bfe099a8Ben Linimport static com.android.documentsui.base.Shared.DEBUG; 21472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 2281afd7f587176e7d63f00d533b1258dfec84bf5cBen Linimport android.annotation.ColorRes; 236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.annotation.Nullable; 245a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKayimport android.database.Cursor; 256fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Handler; 266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Looper; 276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.SystemClock; 28472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.provider.DocumentsContract.Document; 29a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwaimport android.support.v7.widget.GridLayoutManager; 3015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.support.v7.widget.RecyclerView; 31472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Editable; 32472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Spannable; 33472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.KeyListener; 34472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener; 35472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener.Capitalize; 36472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.style.BackgroundColorSpan; 3715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.util.Log; 3815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.KeyEvent; 3915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.View; 40472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.widget.TextView; 4115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 42990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKayimport com.android.documentsui.base.EventListener; 43d9caa6ab53aa784acaf241c0ded3c4ae2d342bf8Steve McKayimport com.android.documentsui.base.Events; 44047182631669608af946480c2545a10acb2ef1bfSteve McKayimport com.android.documentsui.base.Procedure; 4517b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.DocumentHolder; 4617b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.DocumentsAdapter; 4717b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.FocusHandler; 4817b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.Model; 49990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKayimport com.android.documentsui.dirlist.Model.Update; 5075b7b9039cf0efcb188e916c6f510328bfe099a8Ben Linimport com.android.documentsui.selection.SelectionManager; 51472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 52472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.ArrayList; 53472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.List; 546fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.Timer; 556fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.TimerTask; 5615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 57047182631669608af946480c2545a10acb2ef1bfSteve McKay/** A class that handles navigation and focus within the DirectoryFragment. */ 5881afd7f587176e7d63f00d533b1258dfec84bf5cBen Linpublic final class FocusManager implements FocusHandler { 5915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private static final String TAG = "FocusManager"; 6015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 615b0a2c187a9e446b683687817d22cbe443585223Steve McKay private final ContentScope mScope = new ContentScope(); 62047182631669608af946480c2545a10acb2ef1bfSteve McKay 6375b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin private final SelectionManager mSelectionMgr; 64047182631669608af946480c2545a10acb2ef1bfSteve McKay private final DrawerController mDrawer; 65047182631669608af946480c2545a10acb2ef1bfSteve McKay private final Procedure mRootsFocuser; 66047182631669608af946480c2545a10acb2ef1bfSteve McKay private final TitleSearchHelper mSearchHelper; 67047182631669608af946480c2545a10acb2ef1bfSteve McKay 68047182631669608af946480c2545a10acb2ef1bfSteve McKay private boolean mNavDrawerHasFocus; 69047182631669608af946480c2545a10acb2ef1bfSteve McKay 70047182631669608af946480c2545a10acb2ef1bfSteve McKay public FocusManager( 71047182631669608af946480c2545a10acb2ef1bfSteve McKay SelectionManager selectionMgr, 72047182631669608af946480c2545a10acb2ef1bfSteve McKay DrawerController drawer, 73047182631669608af946480c2545a10acb2ef1bfSteve McKay Procedure rootsFocuser, 74047182631669608af946480c2545a10acb2ef1bfSteve McKay @ColorRes int color) { 7574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 7675b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin mSelectionMgr = selectionMgr; 77047182631669608af946480c2545a10acb2ef1bfSteve McKay mDrawer = drawer; 78047182631669608af946480c2545a10acb2ef1bfSteve McKay mRootsFocuser = rootsFocuser; 79047182631669608af946480c2545a10acb2ef1bfSteve McKay 8081afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin mSearchHelper = new TitleSearchHelper(color); 8115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 8215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 8374956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 84047182631669608af946480c2545a10acb2ef1bfSteve McKay public boolean advanceFocusArea() { 85047182631669608af946480c2545a10acb2ef1bfSteve McKay boolean toogleHappened = false; 86047182631669608af946480c2545a10acb2ef1bfSteve McKay if (mNavDrawerHasFocus) { 87047182631669608af946480c2545a10acb2ef1bfSteve McKay mDrawer.setOpen(false); 88047182631669608af946480c2545a10acb2ef1bfSteve McKay focusDirectoryList(); 89047182631669608af946480c2545a10acb2ef1bfSteve McKay } else { 90047182631669608af946480c2545a10acb2ef1bfSteve McKay mDrawer.setOpen(true); 91047182631669608af946480c2545a10acb2ef1bfSteve McKay toogleHappened = mRootsFocuser.run(); 92047182631669608af946480c2545a10acb2ef1bfSteve McKay } 93047182631669608af946480c2545a10acb2ef1bfSteve McKay 94047182631669608af946480c2545a10acb2ef1bfSteve McKay if (toogleHappened) { 95047182631669608af946480c2545a10acb2ef1bfSteve McKay mNavDrawerHasFocus = !mNavDrawerHasFocus; 96047182631669608af946480c2545a10acb2ef1bfSteve McKay return true; 97047182631669608af946480c2545a10acb2ef1bfSteve McKay } 98047182631669608af946480c2545a10acb2ef1bfSteve McKay 99047182631669608af946480c2545a10acb2ef1bfSteve McKay return false; 100047182631669608af946480c2545a10acb2ef1bfSteve McKay } 101047182631669608af946480c2545a10acb2ef1bfSteve McKay 102047182631669608af946480c2545a10acb2ef1bfSteve McKay @Override 10315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 104472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Search helper gets first crack, for doing type-to-focus. 105472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mSearchHelper.handleKey(doc, keyCode, event)) { 106472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 107472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 108472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 10915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 11015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find the target item and focus it. 11115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int endPos = findTargetPosition(doc.itemView, keyCode, event); 11215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 11315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (endPos != RecyclerView.NO_POSITION) { 11415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa focusItem(endPos); 11515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 11674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Swallow all navigation keystrokes. Otherwise they go to the app's global 11774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // key-handler, which will route them back to the DF and cause focus to be reset. 11874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return true; 11974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 12074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return false; 12174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 12274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 12374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa @Override 12474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa public void onFocusChange(View v, boolean hasFocus) { 12574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Remember focus events on items. 1265b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (hasFocus && v.getParent() == mScope.view) { 1275b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.lastFocusPosition = mScope.view.getChildAdapterPosition(v); 12874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 12974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 13074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 13174956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 132047182631669608af946480c2545a10acb2ef1bfSteve McKay public boolean focusDirectoryList() { 1335b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (mScope.adapter.getItemCount() == 0) { 134047182631669608af946480c2545a10acb2ef1bfSteve McKay if (DEBUG) 135047182631669608af946480c2545a10acb2ef1bfSteve McKay Log.v(TAG, "Nothing to focus."); 13675b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin return false; 137237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa } 138237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa 13975b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin // If there's a selection going on, we don't want to grant user the ability to focus 140047182631669608af946480c2545a10acb2ef1bfSteve McKay // on any individfocusSomethingual item to prevent ambiguity in operations (Cut selection 141047182631669608af946480c2545a10acb2ef1bfSteve McKay // vs. Cut focused 14275b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin // item) 14375b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin if (mSelectionMgr.hasSelection()) { 144047182631669608af946480c2545a10acb2ef1bfSteve McKay if (DEBUG) 145047182631669608af946480c2545a10acb2ef1bfSteve McKay Log.v(TAG, "Existing selection found. No focus will be done."); 14675b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin return false; 14715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 14875b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin 14975b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin final int focusPos = (mScope.lastFocusPosition != RecyclerView.NO_POSITION) 150047182631669608af946480c2545a10acb2ef1bfSteve McKay ? mScope.lastFocusPosition 151047182631669608af946480c2545a10acb2ef1bfSteve McKay : mScope.layout.findFirstVisibleItemPosition(); 15275b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin focusItem(focusPos); 15375b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin return true; 15415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 15515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 15681afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin /* 15781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin * Attempts to reset focus on the item corresponding to {@code mPendingFocusId} if it exists and 15881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin * has a valid position in the adapter. It then automatically resets {@code mPendingFocusId}. 15981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin */ 16081afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin @Override 16181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin public void onLayoutCompleted() { 1625b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (mScope.pendingFocusId == null) { 16381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin return; 16481afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 16581afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin 1665b0a2c187a9e446b683687817d22cbe443585223Steve McKay int pos = mScope.adapter.getModelIds().indexOf(mScope.pendingFocusId); 16781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin if (pos != -1) { 16881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin focusItem(pos); 16981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 1705b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.pendingFocusId = null; 17181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 17281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin 17381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin /* 17481afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin * Attempts to put focus on the document associated with the given modelId. If item does not 175047182631669608af946480c2545a10acb2ef1bfSteve McKay * exist yet in the layout, this sets a pending modelId to be used when {@code 176047182631669608af946480c2545a10acb2ef1bfSteve McKay * #applyPendingFocus()} is called next time. 17781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin */ 17881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin @Override 17917b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay public void focusDocument(String modelId) { 1805b0a2c187a9e446b683687817d22cbe443585223Steve McKay int pos = mScope.adapter.getModelIds().indexOf(modelId); 1815b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (pos != -1 && mScope.view.findViewHolderForAdapterPosition(pos) != null) { 18281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin focusItem(pos); 18381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } else { 1845b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.pendingFocusId = modelId; 18581afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 18681afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 18781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin 18874956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 1899504d764fc5b625661959ed6dcd190b9730d418dBen Kwa public int getFocusPosition() { 1905b0a2c187a9e446b683687817d22cbe443585223Steve McKay return mScope.lastFocusPosition; 1919504d764fc5b625661959ed6dcd190b9730d418dBen Kwa } 1929504d764fc5b625661959ed6dcd190b9730d418dBen Kwa 193d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin @Override 19475b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin public boolean hasFocusedItem() { 19575b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin return mScope.lastFocusPosition != RecyclerView.NO_POSITION; 19675b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin } 19775b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin 19875b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin @Override 199d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin public @Nullable String getFocusModelId() { 200d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin if (mScope.lastFocusPosition != RecyclerView.NO_POSITION) { 201d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin DocumentHolder holder = (DocumentHolder) mScope.view 202d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin .findViewHolderForAdapterPosition(mScope.lastFocusPosition); 203d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin return holder.getModelId(); 204d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin } 205d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin return null; 206d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin } 207d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin 2089504d764fc5b625661959ed6dcd190b9730d418dBen Kwa /** 20915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Finds the destination position where the focus should land for a given navigation event. 21015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 21115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the event. 21215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode The key code for the event. 21315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 21415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION. 21515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 21615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findTargetPosition(View view, int keyCode, KeyEvent event) { 21715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 21815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_HOME: 21915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return 0; 22015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_END: 2215b0a2c187a9e446b683687817d22cbe443585223Steve McKay return mScope.adapter.getItemCount() - 1; 22215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_UP: 22315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_DOWN: 22415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return findPagedTargetPosition(view, keyCode, event); 22515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 22615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 22715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find a navigation target based on the arrow key that the user pressed. 22815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int searchDir = -1; 22915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 23015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_UP: 23115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_UP; 23215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 23315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_DOWN: 23415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_DOWN; 23515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 236a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 237a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 238a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (inGridMode()) { 2395b0a2c187a9e446b683687817d22cbe443585223Steve McKay int currentPosition = mScope.view.getChildAdapterPosition(view); 240a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Left and right arrow keys only work in grid mode. 241a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa switch (keyCode) { 242a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_LEFT: 243a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (currentPosition > 0) { 244a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop backward focus search at the first item, otherwise focus will wrap 245a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the last visible item. 246a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_BACKWARD; 247a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 248a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 249a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_RIGHT: 2505b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (currentPosition < mScope.adapter.getItemCount() - 1) { 251a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop forward focus search at the last item, otherwise focus will wrap 252a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the first visible item. 253a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_FORWARD; 254a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 255a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 256a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 25715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 25815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 25915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (searchDir != -1) { 26067f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // Focus search behaves badly if the parent RecyclerView is focused. However, focusable 26167f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after 26267f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable 26367f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // off while performing the focus search. 26467f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // TODO: Revisit this when RV focus issues are resolved. 2655b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view.setFocusable(false); 26615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa View targetView = view.focusSearch(searchDir); 2675b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view.setFocusable(true); 26815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // TargetView can be null, for example, if the user pressed <down> at the bottom 26915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // of the list. 27015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (targetView != null) { 27115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Ignore navigation targets that aren't items in the RecyclerView. 2725b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (targetView.getParent() == mScope.view) { 2735b0a2c187a9e446b683687817d22cbe443585223Steve McKay return mScope.view.getChildAdapterPosition(targetView); 27415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 27515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 27615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 27715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 27815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return RecyclerView.NO_POSITION; 27915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 28015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 28115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 282047182631669608af946480c2545a10acb2ef1bfSteve McKay * Given a PgUp/PgDn event and the current view, find the position of the target view. This 283047182631669608af946480c2545a10acb2ef1bfSteve McKay * returns: 284047182631669608af946480c2545a10acb2ef1bfSteve McKay * <li>The position of the topmost (or bottom-most) visible item, if the current item is not the 285047182631669608af946480c2545a10acb2ef1bfSteve McKay * top- or bottom-most visible item. 28615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The position of an item that is one page's worth of items up (or down) if the current 287047182631669608af946480c2545a10acb2ef1bfSteve McKay * item is the top- or bottom-most visible item. 28815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The first (or last) item, if paging up (or down) would go past those limits. 289047182631669608af946480c2545a10acb2ef1bfSteve McKay * 29015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the key event. 29115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN. 29215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 29315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the target item. 29415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 29515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) { 2965b0a2c187a9e446b683687817d22cbe443585223Steve McKay int first = mScope.layout.findFirstVisibleItemPosition(); 2975b0a2c187a9e446b683687817d22cbe443585223Steve McKay int last = mScope.layout.findLastVisibleItemPosition(); 2985b0a2c187a9e446b683687817d22cbe443585223Steve McKay int current = mScope.view.getChildAdapterPosition(view); 29915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int pageSize = last - first + 1; 30015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 30115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { 30215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current > first) { 30315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the first item, target the first item. 30415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return first; 30515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 30615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the first item, target the item one page up. 30715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current - pageSize; 30815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < 0 ? 0 : target; 30915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 31015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 31115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 31215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) { 31315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current < last) { 31415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the last item, target the last item. 31515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return last; 31615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 31715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the last item, target the item one page down. 31815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current + pageSize; 3195b0a2c187a9e446b683687817d22cbe443585223Steve McKay int max = mScope.adapter.getItemCount() - 1; 32015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < max ? target : max; 32115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 32215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 32315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 32415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa throw new IllegalArgumentException("Unsupported keyCode: " + keyCode); 32515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 32615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 32715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 32815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 32915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * necessary. 33015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 33115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param pos 33215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 33315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private void focusItem(final int pos) { 3346fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa focusItem(pos, null); 3356fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 3366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 3376fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa /** 3386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 3396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * necessary. 3406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 3416fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param pos 3426fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param callback A callback to call after the given item has been focused. 3436fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa */ 344047182631669608af946480c2545a10acb2ef1bfSteve McKay private void focusItem(final int pos, @Nullable 345047182631669608af946480c2545a10acb2ef1bfSteve McKay final FocusCallback callback) { 3465b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (mScope.pendingFocusId != null) { 3475b0a2c187a9e446b683687817d22cbe443585223Steve McKay Log.v(TAG, "clearing pending focus id: " + mScope.pendingFocusId); 3485b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.pendingFocusId = null; 34981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 35081afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin 35115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the item is already in view, focus it; otherwise, scroll to it and focus it. 3525b0a2c187a9e446b683687817d22cbe443585223Steve McKay RecyclerView.ViewHolder vh = mScope.view.findViewHolderForAdapterPosition(pos); 35315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 3546fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 3556fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa callback.onFocus(vh.itemView); 3566fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 35715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 35815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Set a one-time listener to request focus when the scroll has completed. 3595b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view.addOnScrollListener( 36015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa new RecyclerView.OnScrollListener() { 36115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa @Override 36215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public void onScrollStateChanged(RecyclerView view, int newState) { 36315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (newState == RecyclerView.SCROLL_STATE_IDLE) { 36415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // When scrolling stops, find the item and focus it. 365047182631669608af946480c2545a10acb2ef1bfSteve McKay RecyclerView.ViewHolder vh = view 366047182631669608af946480c2545a10acb2ef1bfSteve McKay .findViewHolderForAdapterPosition(pos); 36715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 3686fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 3696fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa callback.onFocus(vh.itemView); 3706fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 37115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 37215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // This might happen in weird corner cases, e.g. if the user is 37315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // scrolling while a delete operation is in progress. In that 37415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // case, just don't attempt to focus the missing item. 37515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa Log.w(TAG, "Unable to focus position " + pos + " after scroll"); 37615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 37715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa view.removeOnScrollListener(this); 37815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 37915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 38015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa }); 3815b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view.smoothScrollToPosition(pos); 38215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 38315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 384a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 385047182631669608af946480c2545a10acb2ef1bfSteve McKay /** @return Whether the layout manager is currently in a grid-configuration. */ 386a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa private boolean inGridMode() { 3875b0a2c187a9e446b683687817d22cbe443585223Steve McKay return mScope.layout.getSpanCount() > 1; 388a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 389472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 3906fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private interface FocusCallback { 3916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void onFocus(View view); 3926fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 3936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 394472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 395472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via 396472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build 397472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * up a string from individual key events, and perform searching based on that string. When an 398472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * item is found that matches the search term, that item will be focused. This class also 399472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * highlights instances of the search term found in the view. 400472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 401472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private class TitleSearchHelper { 402047182631669608af946480c2545a10acb2ef1bfSteve McKay private static final int SEARCH_TIMEOUT = 500; // ms 4036fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 4046fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false); 4056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final Editable mSearchString = Editable.Factory.getInstance().newEditable(""); 4066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final Highlighter mHighlighter = new Highlighter(); 4076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final BackgroundColorSpan mSpan; 4086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 409472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private List<String> mIndex; 410472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private boolean mActive; 4116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Timer mTimer; 4126fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private KeyEvent mLastEvent; 4136fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Handler mUiRunner; 414472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 41581afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin public TitleSearchHelper(@ColorRes int color) { 41681afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin mSpan = new BackgroundColorSpan(color); 4176fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Handler for running things on the main UI thread. Needed for updating the UI from a 4186fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // timer (see #activate, below). 4196fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mUiRunner = new Handler(Looper.getMainLooper()); 420472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 421472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 422472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 423472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out 424472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * of individual key events, and then performs a search for the given string. 425472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * 426472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param doc The document holder receiving the key event. 427472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param keyCode 428472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param event 429472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @return Whether the event was handled. 430472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 431472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 432472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa switch (keyCode) { 433472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ESCAPE: 434472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ENTER: 435472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 436472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // These keys end any active searches. 4376fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 438472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 439472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 440472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle these key events if there is no active search. 441472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 442472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 443472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_SPACE: 444472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // This allows users to search for files with spaces in their names, but ignores 4456fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // spacebar events when a text search is not active. Ignoring the spacebar 4466fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // event is necessary because other handlers (see FocusManager#handleKey) also 4476fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // listen for and handle it. 448472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 449472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 450472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 451472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 452472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 453472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Navigation keys also end active searches. 454472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 4556fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 456472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle the keycode, so navigation still occurs. 457472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 458472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 459472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 460472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Build up the search string, and perform the search. 461472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event); 462472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 463472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Delete is processed by the text listener, but not "handled". Check separately for it. 4646fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (keyCode == KeyEvent.KEYCODE_DEL) { 4656fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa handled = true; 4666fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 4676fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 4686fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (handled) { 4696fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mLastEvent = event; 4706fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mSearchString.length() == 0) { 471472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't perform empty searches. 472472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 473472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 4746fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa search(); 475472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 476472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 477472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return handled; 478472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 479472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 480472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 481472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Activates the search helper, which changes its key handling and updates the search index 482472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * and highlights if necessary. Call this each time the search term is updated. 483472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 4846fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void search() { 485472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 4866fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // The model listener invalidates the search index when the model changes. 4875b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.model.addUpdateListener(mModelListener); 4886fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 4896fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Used to keep the current search alive until the timeout expires. If the user 4906fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // presses another key within that time, that keystroke is added to the current 4916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // search. Otherwise, the current search ends, and subsequent keystrokes start a new 4926fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // search. 4936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer = new Timer(); 4946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mActive = true; 495472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 496472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 497472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // If the search index was invalidated, rebuild it 498472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mIndex == null) { 499472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa buildIndex(); 500472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 501472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5026fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Search for the current search term. 5036fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Perform case-insensitive search. 5046fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String searchString = mSearchString.toString().toLowerCase(); 5056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa for (int pos = 0; pos < mIndex.size(); pos++) { 5066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String title = mIndex.get(pos); 5076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (title != null && title.startsWith(searchString)) { 508047182631669608af946480c2545a10acb2ef1bfSteve McKay focusItem( 509047182631669608af946480c2545a10acb2ef1bfSteve McKay pos, 510047182631669608af946480c2545a10acb2ef1bfSteve McKay new FocusCallback() { 511047182631669608af946480c2545a10acb2ef1bfSteve McKay @Override 512047182631669608af946480c2545a10acb2ef1bfSteve McKay public void onFocus(View view) { 513047182631669608af946480c2545a10acb2ef1bfSteve McKay mHighlighter.applyHighlight(view); 514047182631669608af946480c2545a10acb2ef1bfSteve McKay // Using a timer repeat period of SEARCH_TIMEOUT/2 means the 515047182631669608af946480c2545a10acb2ef1bfSteve McKay // amount of 516047182631669608af946480c2545a10acb2ef1bfSteve McKay // time between the last keystroke and a search expiring is 517047182631669608af946480c2545a10acb2ef1bfSteve McKay // actually 518047182631669608af946480c2545a10acb2ef1bfSteve McKay // between 500 and 750 ms. A smaller timer period results in 519047182631669608af946480c2545a10acb2ef1bfSteve McKay // less 520047182631669608af946480c2545a10acb2ef1bfSteve McKay // variability but does more polling. 521047182631669608af946480c2545a10acb2ef1bfSteve McKay mTimer.schedule(new TimeoutTask(), 0, SEARCH_TIMEOUT / 2); 522047182631669608af946480c2545a10acb2ef1bfSteve McKay } 523047182631669608af946480c2545a10acb2ef1bfSteve McKay }); 5246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa break; 5256fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 5266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 527472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 528472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 529047182631669608af946480c2545a10acb2ef1bfSteve McKay /** Ends the current search (see {@link #search()}. */ 5306fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void endSearch() { 531472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 5325b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.model.removeUpdateListener(mModelListener); 5336fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer.cancel(); 534472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 535472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mHighlighter.removeHighlight(); 537472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 538472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 539472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSearchString.clear(); 540472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mActive = false; 541472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 542472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 543472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 544472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Builds a search index for finding items by title. Queries the model and adapter, so both 545472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * must be set up before calling this method. 546472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 547472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void buildIndex() { 5485b0a2c187a9e446b683687817d22cbe443585223Steve McKay int itemCount = mScope.adapter.getItemCount(); 549472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa List<String> index = new ArrayList<>(itemCount); 550472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa for (int i = 0; i < itemCount; i++) { 5515b0a2c187a9e446b683687817d22cbe443585223Steve McKay String modelId = mScope.adapter.getModelId(i); 5525b0a2c187a9e446b683687817d22cbe443585223Steve McKay Cursor cursor = mScope.model.getItem(modelId); 5535a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay if (modelId != null && cursor != null) { 5545a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay String title = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 5556fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Perform case-insensitive search. 5566fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa index.add(title.toLowerCase()); 557472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 558472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa index.add(""); 559472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 560472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 561472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = index; 562472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 563472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 564990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKay private EventListener<Model.Update> mModelListener = new EventListener<Model.Update>() { 565472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 566990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKay public void accept(Update event) { 567472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Invalidate the search index when the model updates. 568472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 569472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 570472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 571472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private class TimeoutTask extends TimerTask { 5736fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa @Override 5746fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void run() { 5756fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa long last = mLastEvent.getEventTime(); 5766fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa long now = SystemClock.uptimeMillis(); 5776fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if ((now - last) > SEARCH_TIMEOUT) { 5786fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // endSearch must run on the main thread because it does UI work 579047182631669608af946480c2545a10acb2ef1bfSteve McKay mUiRunner.post( 580047182631669608af946480c2545a10acb2ef1bfSteve McKay new Runnable() { 581047182631669608af946480c2545a10acb2ef1bfSteve McKay @Override 582047182631669608af946480c2545a10acb2ef1bfSteve McKay public void run() { 583047182631669608af946480c2545a10acb2ef1bfSteve McKay endSearch(); 584047182631669608af946480c2545a10acb2ef1bfSteve McKay } 585047182631669608af946480c2545a10acb2ef1bfSteve McKay }); 586472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 587472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 5886fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa }; 5896fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 5906fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private class Highlighter { 5916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Spannable mCurrentHighlight; 592472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 593472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 594047182631669608af946480c2545a10acb2ef1bfSteve McKay * Applies title highlights to the given view. The view must have a title field that is 595047182631669608af946480c2545a10acb2ef1bfSteve McKay * a spannable text field. If this condition is not met, this function does nothing. 5966fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 5976fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param view 598472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 5996fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void applyHighlight(View view) { 6006fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa TextView titleView = (TextView) view.findViewById(android.R.id.title); 6016fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (titleView == null) { 6026fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa return; 603472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 604472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 6056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa CharSequence tmpText = titleView.getText(); 6066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (tmpText instanceof Spannable) { 6076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mCurrentHighlight != null) { 6086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.removeSpan(mSpan); 6096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 6106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight = (Spannable) tmpText; 6116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.setSpan( 6126fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mSpan, 0, mSearchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 6136fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 614472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 615472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 6166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa /** 617047182631669608af946480c2545a10acb2ef1bfSteve McKay * Removes title highlights from the given view. The view must have a title field that 618047182631669608af946480c2545a10acb2ef1bfSteve McKay * is a spannable text field. If this condition is not met, this function does nothing. 6196fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 6206fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param view 6216fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa */ 6226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void removeHighlight() { 6236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mCurrentHighlight != null) { 6246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.removeSpan(mSpan); 625472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 626472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 627472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 628472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 62917b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 63017b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay public FocusManager reset(RecyclerView view, Model model) { 6315b0a2c187a9e446b683687817d22cbe443585223Steve McKay assert (view != null); 6325b0a2c187a9e446b683687817d22cbe443585223Steve McKay assert (model != null); 6335b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view = view; 6345b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.adapter = (DocumentsAdapter) view.getAdapter(); 6355b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.layout = (GridLayoutManager) view.getLayoutManager(); 6365b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.model = model; 63717b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 6385b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.lastFocusPosition = RecyclerView.NO_POSITION; 6395b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.pendingFocusId = null; 64017b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 6415b0a2c187a9e446b683687817d22cbe443585223Steve McKay return this; 6425b0a2c187a9e446b683687817d22cbe443585223Steve McKay } 64317b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 6445b0a2c187a9e446b683687817d22cbe443585223Steve McKay private static final class ContentScope { 6455b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable RecyclerView view; 6465b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable DocumentsAdapter adapter; 6475b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable GridLayoutManager layout; 6485b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable Model model; 64917b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 6505b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable String pendingFocusId; 6515b0a2c187a9e446b683687817d22cbe443585223Steve McKay private int lastFocusPosition = RecyclerView.NO_POSITION; 65217b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay } 65315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa} 654