FocusManager.java revision e967033315ed64bca8c89d601d187fd12754f1fb
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; 2198f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKayimport static com.android.internal.util.Preconditions.checkNotNull; 22472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 2381afd7f587176e7d63f00d533b1258dfec84bf5cBen Linimport android.annotation.ColorRes; 246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.annotation.Nullable; 255a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKayimport android.database.Cursor; 266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Handler; 276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Looper; 286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.SystemClock; 29472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.provider.DocumentsContract.Document; 30a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwaimport android.support.v7.widget.GridLayoutManager; 3115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.support.v7.widget.RecyclerView; 32472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Editable; 33472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Spannable; 34472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.KeyListener; 35472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener; 36472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener.Capitalize; 37472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.style.BackgroundColorSpan; 3815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.util.Log; 3915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.KeyEvent; 4015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.View; 41472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.widget.TextView; 4215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 43990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKayimport com.android.documentsui.base.EventListener; 44d9caa6ab53aa784acaf241c0ded3c4ae2d342bf8Steve McKayimport com.android.documentsui.base.Events; 4598f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKayimport com.android.documentsui.base.Features; 46047182631669608af946480c2545a10acb2ef1bfSteve McKayimport com.android.documentsui.base.Procedure; 4717b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.DocumentHolder; 4817b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.DocumentsAdapter; 4917b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.FocusHandler; 50e967033315ed64bca8c89d601d187fd12754f1fbGarfield Tanimport com.android.documentsui.Model.Update; 5175b7b9039cf0efcb188e916c6f510328bfe099a8Ben Linimport com.android.documentsui.selection.SelectionManager; 52472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 53472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.ArrayList; 54472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.List; 556fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.Timer; 566fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.TimerTask; 5715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 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 6398f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKay private final Features mFeatures; 6475b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin private final SelectionManager mSelectionMgr; 65047182631669608af946480c2545a10acb2ef1bfSteve McKay private final DrawerController mDrawer; 66047182631669608af946480c2545a10acb2ef1bfSteve McKay private final Procedure mRootsFocuser; 67047182631669608af946480c2545a10acb2ef1bfSteve McKay private final TitleSearchHelper mSearchHelper; 68047182631669608af946480c2545a10acb2ef1bfSteve McKay 69047182631669608af946480c2545a10acb2ef1bfSteve McKay private boolean mNavDrawerHasFocus; 70047182631669608af946480c2545a10acb2ef1bfSteve McKay 71047182631669608af946480c2545a10acb2ef1bfSteve McKay public FocusManager( 7298f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKay Features features, 73047182631669608af946480c2545a10acb2ef1bfSteve McKay SelectionManager selectionMgr, 74047182631669608af946480c2545a10acb2ef1bfSteve McKay DrawerController drawer, 75047182631669608af946480c2545a10acb2ef1bfSteve McKay Procedure rootsFocuser, 76047182631669608af946480c2545a10acb2ef1bfSteve McKay @ColorRes int color) { 7774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 7898f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKay mFeatures = checkNotNull(features); 7975b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin mSelectionMgr = selectionMgr; 80047182631669608af946480c2545a10acb2ef1bfSteve McKay mDrawer = drawer; 81047182631669608af946480c2545a10acb2ef1bfSteve McKay mRootsFocuser = rootsFocuser; 82047182631669608af946480c2545a10acb2ef1bfSteve McKay 8381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin mSearchHelper = new TitleSearchHelper(color); 8415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 8515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 8674956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 87047182631669608af946480c2545a10acb2ef1bfSteve McKay public boolean advanceFocusArea() { 88340ab17f468789bb507daeae116cf7940ba84b03Ben Lin // This should only be called in pre-O devices. 89340ab17f468789bb507daeae116cf7940ba84b03Ben Lin // O has built-in keyboard navigation support. 9098f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKay assert(!mFeatures.isSystemKeyboardNavigationEnabled()); 9192ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay boolean focusChanged = false; 92047182631669608af946480c2545a10acb2ef1bfSteve McKay if (mNavDrawerHasFocus) { 93047182631669608af946480c2545a10acb2ef1bfSteve McKay mDrawer.setOpen(false); 94b62d4e5804d807703697ad7eeb85131a35ce4ab4Ben Lin focusChanged = focusDirectoryList(); 95047182631669608af946480c2545a10acb2ef1bfSteve McKay } else { 96047182631669608af946480c2545a10acb2ef1bfSteve McKay mDrawer.setOpen(true); 9792ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay focusChanged = mRootsFocuser.run(); 98047182631669608af946480c2545a10acb2ef1bfSteve McKay } 99047182631669608af946480c2545a10acb2ef1bfSteve McKay 10092ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay if (focusChanged) { 101047182631669608af946480c2545a10acb2ef1bfSteve McKay mNavDrawerHasFocus = !mNavDrawerHasFocus; 102047182631669608af946480c2545a10acb2ef1bfSteve McKay return true; 103047182631669608af946480c2545a10acb2ef1bfSteve McKay } 104047182631669608af946480c2545a10acb2ef1bfSteve McKay 105047182631669608af946480c2545a10acb2ef1bfSteve McKay return false; 106047182631669608af946480c2545a10acb2ef1bfSteve McKay } 107047182631669608af946480c2545a10acb2ef1bfSteve McKay 108047182631669608af946480c2545a10acb2ef1bfSteve McKay @Override 10915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 110472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Search helper gets first crack, for doing type-to-focus. 111472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mSearchHelper.handleKey(doc, keyCode, event)) { 112472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 113472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 114472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 11515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 11615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find the target item and focus it. 11715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int endPos = findTargetPosition(doc.itemView, keyCode, event); 11815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 11915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (endPos != RecyclerView.NO_POSITION) { 12015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa focusItem(endPos); 12115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 12274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Swallow all navigation keystrokes. Otherwise they go to the app's global 12374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // key-handler, which will route them back to the DF and cause focus to be reset. 12474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return true; 12574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 12674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return false; 12774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 12874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 12974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa @Override 13074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa public void onFocusChange(View v, boolean hasFocus) { 13174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Remember focus events on items. 1325b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (hasFocus && v.getParent() == mScope.view) { 1335b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.lastFocusPosition = mScope.view.getChildAdapterPosition(v); 13474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 13574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 13674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 13774956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 138047182631669608af946480c2545a10acb2ef1bfSteve McKay public boolean focusDirectoryList() { 1395b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (mScope.adapter.getItemCount() == 0) { 14092ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay if (DEBUG) Log.v(TAG, "Nothing to focus."); 14175b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin return false; 142237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa } 143237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa 14475b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin // If there's a selection going on, we don't want to grant user the ability to focus 145047182631669608af946480c2545a10acb2ef1bfSteve McKay // on any individfocusSomethingual item to prevent ambiguity in operations (Cut selection 146047182631669608af946480c2545a10acb2ef1bfSteve McKay // vs. Cut focused 14775b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin // item) 14875b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin if (mSelectionMgr.hasSelection()) { 14992ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay if (DEBUG) Log.v(TAG, "Existing selection found. No focus will be done."); 15075b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin return false; 15115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 15275b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin 15375b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin final int focusPos = (mScope.lastFocusPosition != RecyclerView.NO_POSITION) 154047182631669608af946480c2545a10acb2ef1bfSteve McKay ? mScope.lastFocusPosition 155047182631669608af946480c2545a10acb2ef1bfSteve McKay : mScope.layout.findFirstVisibleItemPosition(); 15675b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin focusItem(focusPos); 15775b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin return true; 15815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 15915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 16081afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin /* 16181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin * Attempts to reset focus on the item corresponding to {@code mPendingFocusId} if it exists and 16281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin * has a valid position in the adapter. It then automatically resets {@code mPendingFocusId}. 16381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin */ 16481afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin @Override 16581afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin public void onLayoutCompleted() { 1665b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (mScope.pendingFocusId == null) { 16781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin return; 16881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 16981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin 1705b0a2c187a9e446b683687817d22cbe443585223Steve McKay int pos = mScope.adapter.getModelIds().indexOf(mScope.pendingFocusId); 17181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin if (pos != -1) { 17281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin focusItem(pos); 17381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 1745b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.pendingFocusId = null; 17581afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 17681afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin 17781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin /* 17881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin * Attempts to put focus on the document associated with the given modelId. If item does not 179047182631669608af946480c2545a10acb2ef1bfSteve McKay * exist yet in the layout, this sets a pending modelId to be used when {@code 180047182631669608af946480c2545a10acb2ef1bfSteve McKay * #applyPendingFocus()} is called next time. 18181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin */ 18281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin @Override 18317b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay public void focusDocument(String modelId) { 1845b0a2c187a9e446b683687817d22cbe443585223Steve McKay int pos = mScope.adapter.getModelIds().indexOf(modelId); 1855b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (pos != -1 && mScope.view.findViewHolderForAdapterPosition(pos) != null) { 18681afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin focusItem(pos); 18781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } else { 1885b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.pendingFocusId = modelId; 18981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 19081afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 19181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin 19274956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 1939504d764fc5b625661959ed6dcd190b9730d418dBen Kwa public int getFocusPosition() { 1945b0a2c187a9e446b683687817d22cbe443585223Steve McKay return mScope.lastFocusPosition; 1959504d764fc5b625661959ed6dcd190b9730d418dBen Kwa } 1969504d764fc5b625661959ed6dcd190b9730d418dBen Kwa 197d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin @Override 19875b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin public boolean hasFocusedItem() { 19975b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin return mScope.lastFocusPosition != RecyclerView.NO_POSITION; 20075b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin } 20175b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin 20275b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin @Override 203d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin public @Nullable String getFocusModelId() { 204d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin if (mScope.lastFocusPosition != RecyclerView.NO_POSITION) { 205d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin DocumentHolder holder = (DocumentHolder) mScope.view 206d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin .findViewHolderForAdapterPosition(mScope.lastFocusPosition); 207d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin return holder.getModelId(); 208d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin } 209d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin return null; 210d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin } 211d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin 2129504d764fc5b625661959ed6dcd190b9730d418dBen Kwa /** 21315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Finds the destination position where the focus should land for a given navigation event. 21415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 21515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the event. 21615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode The key code for the event. 21715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 21815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION. 21915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 22015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findTargetPosition(View view, int keyCode, KeyEvent event) { 22115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 22215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_HOME: 22315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return 0; 22415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_END: 2255b0a2c187a9e446b683687817d22cbe443585223Steve McKay return mScope.adapter.getItemCount() - 1; 22615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_UP: 22715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_DOWN: 22815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return findPagedTargetPosition(view, keyCode, event); 22915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 23015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 23115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find a navigation target based on the arrow key that the user pressed. 23215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int searchDir = -1; 23315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 23415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_UP: 23515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_UP; 23615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 23715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_DOWN: 23815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_DOWN; 23915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 240a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 241a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 242a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (inGridMode()) { 2435b0a2c187a9e446b683687817d22cbe443585223Steve McKay int currentPosition = mScope.view.getChildAdapterPosition(view); 244a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Left and right arrow keys only work in grid mode. 245a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa switch (keyCode) { 246a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_LEFT: 247a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (currentPosition > 0) { 248a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop backward focus search at the first item, otherwise focus will wrap 249a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the last visible item. 250a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_BACKWARD; 251a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 252a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 253a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_RIGHT: 2545b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (currentPosition < mScope.adapter.getItemCount() - 1) { 255a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop forward focus search at the last item, otherwise focus will wrap 256a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the first visible item. 257a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_FORWARD; 258a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 259a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 260a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 26115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 26215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 26315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (searchDir != -1) { 26467f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // Focus search behaves badly if the parent RecyclerView is focused. However, focusable 26567f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after 26667f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable 26767f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // off while performing the focus search. 26867f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // TODO: Revisit this when RV focus issues are resolved. 2695b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view.setFocusable(false); 27015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa View targetView = view.focusSearch(searchDir); 2715b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view.setFocusable(true); 27215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // TargetView can be null, for example, if the user pressed <down> at the bottom 27315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // of the list. 27415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (targetView != null) { 27515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Ignore navigation targets that aren't items in the RecyclerView. 2765b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (targetView.getParent() == mScope.view) { 2775b0a2c187a9e446b683687817d22cbe443585223Steve McKay return mScope.view.getChildAdapterPosition(targetView); 27815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 27915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 28015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 28115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 28215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return RecyclerView.NO_POSITION; 28315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 28415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 28515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 286047182631669608af946480c2545a10acb2ef1bfSteve McKay * Given a PgUp/PgDn event and the current view, find the position of the target view. This 287047182631669608af946480c2545a10acb2ef1bfSteve McKay * returns: 288047182631669608af946480c2545a10acb2ef1bfSteve McKay * <li>The position of the topmost (or bottom-most) visible item, if the current item is not the 289047182631669608af946480c2545a10acb2ef1bfSteve McKay * top- or bottom-most visible item. 29015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The position of an item that is one page's worth of items up (or down) if the current 291047182631669608af946480c2545a10acb2ef1bfSteve McKay * item is the top- or bottom-most visible item. 29215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The first (or last) item, if paging up (or down) would go past those limits. 293047182631669608af946480c2545a10acb2ef1bfSteve McKay * 29415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the key event. 29515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN. 29615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 29715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the target item. 29815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 29915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) { 3005b0a2c187a9e446b683687817d22cbe443585223Steve McKay int first = mScope.layout.findFirstVisibleItemPosition(); 3015b0a2c187a9e446b683687817d22cbe443585223Steve McKay int last = mScope.layout.findLastVisibleItemPosition(); 3025b0a2c187a9e446b683687817d22cbe443585223Steve McKay int current = mScope.view.getChildAdapterPosition(view); 30315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int pageSize = last - first + 1; 30415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 30515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { 30615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current > first) { 30715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the first item, target the first item. 30815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return first; 30915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 31015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the first item, target the item one page up. 31115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current - pageSize; 31215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < 0 ? 0 : target; 31315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 31415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 31515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 31615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) { 31715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current < last) { 31815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the last item, target the last item. 31915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return last; 32015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 32115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the last item, target the item one page down. 32215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current + pageSize; 3235b0a2c187a9e446b683687817d22cbe443585223Steve McKay int max = mScope.adapter.getItemCount() - 1; 32415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < max ? target : max; 32515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 32615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 32715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 32815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa throw new IllegalArgumentException("Unsupported keyCode: " + keyCode); 32915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 33015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 33115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 33215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 33315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * necessary. 33415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 33515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param pos 33615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 33715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private void focusItem(final int pos) { 3386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa focusItem(pos, null); 3396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 3406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 3416fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa /** 3426fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 3436fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * necessary. 3446fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 3456fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param pos 3466fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param callback A callback to call after the given item has been focused. 3476fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa */ 34892ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay private void focusItem(final int pos, @Nullable final FocusCallback callback) { 3495b0a2c187a9e446b683687817d22cbe443585223Steve McKay if (mScope.pendingFocusId != null) { 3505b0a2c187a9e446b683687817d22cbe443585223Steve McKay Log.v(TAG, "clearing pending focus id: " + mScope.pendingFocusId); 3515b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.pendingFocusId = null; 35281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin } 35381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin 35415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the item is already in view, focus it; otherwise, scroll to it and focus it. 3555b0a2c187a9e446b683687817d22cbe443585223Steve McKay RecyclerView.ViewHolder vh = mScope.view.findViewHolderForAdapterPosition(pos); 35615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 3576fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 3586fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa callback.onFocus(vh.itemView); 3596fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 36015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 36115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Set a one-time listener to request focus when the scroll has completed. 3625b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view.addOnScrollListener( 36315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa new RecyclerView.OnScrollListener() { 36415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa @Override 36515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public void onScrollStateChanged(RecyclerView view, int newState) { 36615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (newState == RecyclerView.SCROLL_STATE_IDLE) { 36715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // When scrolling stops, find the item and focus it. 368047182631669608af946480c2545a10acb2ef1bfSteve McKay RecyclerView.ViewHolder vh = view 369047182631669608af946480c2545a10acb2ef1bfSteve McKay .findViewHolderForAdapterPosition(pos); 37015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 3716fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 3726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa callback.onFocus(vh.itemView); 3736fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 37415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 37515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // This might happen in weird corner cases, e.g. if the user is 37615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // scrolling while a delete operation is in progress. In that 37715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // case, just don't attempt to focus the missing item. 37815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa Log.w(TAG, "Unable to focus position " + pos + " after scroll"); 37915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 38015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa view.removeOnScrollListener(this); 38115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 38215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 38315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa }); 3845b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view.smoothScrollToPosition(pos); 38515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 38615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 387a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 388047182631669608af946480c2545a10acb2ef1bfSteve McKay /** @return Whether the layout manager is currently in a grid-configuration. */ 389a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa private boolean inGridMode() { 3905b0a2c187a9e446b683687817d22cbe443585223Steve McKay return mScope.layout.getSpanCount() > 1; 391a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 392472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 3936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private interface FocusCallback { 3946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void onFocus(View view); 3956fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 3966fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 397472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 398472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via 399472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build 400472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * up a string from individual key events, and perform searching based on that string. When an 401472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * item is found that matches the search term, that item will be focused. This class also 402472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * highlights instances of the search term found in the view. 403472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 404472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private class TitleSearchHelper { 405047182631669608af946480c2545a10acb2ef1bfSteve McKay private static final int SEARCH_TIMEOUT = 500; // ms 4066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 4076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false); 4086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final Editable mSearchString = Editable.Factory.getInstance().newEditable(""); 4096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final Highlighter mHighlighter = new Highlighter(); 4106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final BackgroundColorSpan mSpan; 4116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 412472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private List<String> mIndex; 413472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private boolean mActive; 4146fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Timer mTimer; 4156fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private KeyEvent mLastEvent; 4166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Handler mUiRunner; 417472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 41881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin public TitleSearchHelper(@ColorRes int color) { 41981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin mSpan = new BackgroundColorSpan(color); 4206fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Handler for running things on the main UI thread. Needed for updating the UI from a 4216fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // timer (see #activate, below). 4226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mUiRunner = new Handler(Looper.getMainLooper()); 423472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 424472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 425472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 426472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out 427472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * of individual key events, and then performs a search for the given string. 428472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * 429472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param doc The document holder receiving the key event. 430472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param keyCode 431472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param event 432472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @return Whether the event was handled. 433472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 434472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 435472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa switch (keyCode) { 436472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ESCAPE: 437472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ENTER: 438472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 439472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // These keys end any active searches. 4406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 441472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 442472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 443472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle these key events if there is no active search. 444472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 445472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 446472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_SPACE: 447472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // This allows users to search for files with spaces in their names, but ignores 4486fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // spacebar events when a text search is not active. Ignoring the spacebar 4496fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // event is necessary because other handlers (see FocusManager#handleKey) also 4506fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // listen for and handle it. 451472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 452472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 453472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 454472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 455472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 456472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Navigation keys also end active searches. 457472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 4586fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 459472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle the keycode, so navigation still occurs. 460472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 461472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 462472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 463472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Build up the search string, and perform the search. 464472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event); 465472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 466472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Delete is processed by the text listener, but not "handled". Check separately for it. 4676fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (keyCode == KeyEvent.KEYCODE_DEL) { 4686fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa handled = true; 4696fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 4706fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 4716fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (handled) { 4726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mLastEvent = event; 4736fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mSearchString.length() == 0) { 474472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't perform empty searches. 475472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 476472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 4776fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa search(); 478472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 479472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 480472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return handled; 481472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 482472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 483472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 484472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Activates the search helper, which changes its key handling and updates the search index 485472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * and highlights if necessary. Call this each time the search term is updated. 486472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 4876fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void search() { 488472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 4896fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // The model listener invalidates the search index when the model changes. 4905b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.model.addUpdateListener(mModelListener); 4916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 4926fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Used to keep the current search alive until the timeout expires. If the user 4936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // presses another key within that time, that keystroke is added to the current 4946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // search. Otherwise, the current search ends, and subsequent keystrokes start a new 4956fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // search. 4966fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer = new Timer(); 4976fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mActive = true; 498472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 499472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 500472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // If the search index was invalidated, rebuild it 501472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mIndex == null) { 502472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa buildIndex(); 503472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 504472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Search for the current search term. 5066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Perform case-insensitive search. 5076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String searchString = mSearchString.toString().toLowerCase(); 5086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa for (int pos = 0; pos < mIndex.size(); pos++) { 5096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String title = mIndex.get(pos); 5106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (title != null && title.startsWith(searchString)) { 511047182631669608af946480c2545a10acb2ef1bfSteve McKay focusItem( 512047182631669608af946480c2545a10acb2ef1bfSteve McKay pos, 513047182631669608af946480c2545a10acb2ef1bfSteve McKay new FocusCallback() { 514047182631669608af946480c2545a10acb2ef1bfSteve McKay @Override 515047182631669608af946480c2545a10acb2ef1bfSteve McKay public void onFocus(View view) { 516047182631669608af946480c2545a10acb2ef1bfSteve McKay mHighlighter.applyHighlight(view); 517047182631669608af946480c2545a10acb2ef1bfSteve McKay // Using a timer repeat period of SEARCH_TIMEOUT/2 means the 518047182631669608af946480c2545a10acb2ef1bfSteve McKay // amount of 519047182631669608af946480c2545a10acb2ef1bfSteve McKay // time between the last keystroke and a search expiring is 520047182631669608af946480c2545a10acb2ef1bfSteve McKay // actually 521047182631669608af946480c2545a10acb2ef1bfSteve McKay // between 500 and 750 ms. A smaller timer period results in 522047182631669608af946480c2545a10acb2ef1bfSteve McKay // less 523047182631669608af946480c2545a10acb2ef1bfSteve McKay // variability but does more polling. 524047182631669608af946480c2545a10acb2ef1bfSteve McKay mTimer.schedule(new TimeoutTask(), 0, SEARCH_TIMEOUT / 2); 525047182631669608af946480c2545a10acb2ef1bfSteve McKay } 526047182631669608af946480c2545a10acb2ef1bfSteve McKay }); 5276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa break; 5286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 5296fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 530472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 531472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 532047182631669608af946480c2545a10acb2ef1bfSteve McKay /** Ends the current search (see {@link #search()}. */ 5336fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void endSearch() { 534472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 5355b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.model.removeUpdateListener(mModelListener); 5366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer.cancel(); 537472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 538472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mHighlighter.removeHighlight(); 540472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 541472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 542472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSearchString.clear(); 543472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mActive = false; 544472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 545472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 546472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 547472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Builds a search index for finding items by title. Queries the model and adapter, so both 548472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * must be set up before calling this method. 549472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 550472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void buildIndex() { 5515b0a2c187a9e446b683687817d22cbe443585223Steve McKay int itemCount = mScope.adapter.getItemCount(); 552472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa List<String> index = new ArrayList<>(itemCount); 553472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa for (int i = 0; i < itemCount; i++) { 5545b0a2c187a9e446b683687817d22cbe443585223Steve McKay String modelId = mScope.adapter.getModelId(i); 5555b0a2c187a9e446b683687817d22cbe443585223Steve McKay Cursor cursor = mScope.model.getItem(modelId); 5565a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay if (modelId != null && cursor != null) { 5575a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay String title = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 5586fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Perform case-insensitive search. 5596fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa index.add(title.toLowerCase()); 560472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 561472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa index.add(""); 562472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 563472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 564472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = index; 565472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 566472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 567990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKay private EventListener<Model.Update> mModelListener = new EventListener<Model.Update>() { 568472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 569990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKay public void accept(Update event) { 570472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Invalidate the search index when the model updates. 571472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 572472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 573472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 574472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5756fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private class TimeoutTask extends TimerTask { 5766fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa @Override 5776fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void run() { 5786fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa long last = mLastEvent.getEventTime(); 5796fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa long now = SystemClock.uptimeMillis(); 5806fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if ((now - last) > SEARCH_TIMEOUT) { 5816fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // endSearch must run on the main thread because it does UI work 582047182631669608af946480c2545a10acb2ef1bfSteve McKay mUiRunner.post( 583047182631669608af946480c2545a10acb2ef1bfSteve McKay new Runnable() { 584047182631669608af946480c2545a10acb2ef1bfSteve McKay @Override 585047182631669608af946480c2545a10acb2ef1bfSteve McKay public void run() { 586047182631669608af946480c2545a10acb2ef1bfSteve McKay endSearch(); 587047182631669608af946480c2545a10acb2ef1bfSteve McKay } 588047182631669608af946480c2545a10acb2ef1bfSteve McKay }); 589472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 590472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 5916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa }; 5926fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 5936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private class Highlighter { 5946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Spannable mCurrentHighlight; 595472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 596472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 597047182631669608af946480c2545a10acb2ef1bfSteve McKay * Applies title highlights to the given view. The view must have a title field that is 598047182631669608af946480c2545a10acb2ef1bfSteve McKay * a spannable text field. If this condition is not met, this function does nothing. 5996fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 6006fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param view 601472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 6026fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void applyHighlight(View view) { 6036fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa TextView titleView = (TextView) view.findViewById(android.R.id.title); 6046fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (titleView == null) { 6056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa return; 606472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 607472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 6086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa CharSequence tmpText = titleView.getText(); 6096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (tmpText instanceof Spannable) { 6106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mCurrentHighlight != null) { 6116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.removeSpan(mSpan); 6126fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 6136fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight = (Spannable) tmpText; 6146fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.setSpan( 6156fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mSpan, 0, mSearchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 6166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 617472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 618472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 6196fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa /** 620047182631669608af946480c2545a10acb2ef1bfSteve McKay * Removes title highlights from the given view. The view must have a title field that 621047182631669608af946480c2545a10acb2ef1bfSteve McKay * is a spannable text field. If this condition is not met, this function does nothing. 6226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 6236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param view 6246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa */ 6256fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void removeHighlight() { 6266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mCurrentHighlight != null) { 6276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.removeSpan(mSpan); 628472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 629472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 630472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 631472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 63217b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 63317b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay public FocusManager reset(RecyclerView view, Model model) { 6345b0a2c187a9e446b683687817d22cbe443585223Steve McKay assert (view != null); 6355b0a2c187a9e446b683687817d22cbe443585223Steve McKay assert (model != null); 6365b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.view = view; 6375b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.adapter = (DocumentsAdapter) view.getAdapter(); 6385b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.layout = (GridLayoutManager) view.getLayoutManager(); 6395b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.model = model; 64017b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 6415b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.lastFocusPosition = RecyclerView.NO_POSITION; 6425b0a2c187a9e446b683687817d22cbe443585223Steve McKay mScope.pendingFocusId = null; 64317b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 6445b0a2c187a9e446b683687817d22cbe443585223Steve McKay return this; 6455b0a2c187a9e446b683687817d22cbe443585223Steve McKay } 64617b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 6475b0a2c187a9e446b683687817d22cbe443585223Steve McKay private static final class ContentScope { 6485b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable RecyclerView view; 6495b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable DocumentsAdapter adapter; 6505b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable GridLayoutManager layout; 6515b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable Model model; 65217b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay 6535b0a2c187a9e446b683687817d22cbe443585223Steve McKay private @Nullable String pendingFocusId; 6545b0a2c187a9e446b683687817d22cbe443585223Steve McKay private int lastFocusPosition = RecyclerView.NO_POSITION; 65517b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay } 65615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa} 657