FocusManager.java revision 6fd431ee18b8d6ecaa2620229d3b40bcdf85e370
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 1715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwapackage com.android.documentsui.dirlist; 1815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 19472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport static com.android.documentsui.model.DocumentInfo.getCursorString; 20472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 216fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.annotation.Nullable; 22472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.content.Context; 236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Handler; 246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Looper; 256fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.SystemClock; 26472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.provider.DocumentsContract.Document; 27a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwaimport android.support.v7.widget.GridLayoutManager; 2815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.support.v7.widget.RecyclerView; 29472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Editable; 30472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Spannable; 31472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.KeyListener; 32472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener; 33472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener.Capitalize; 34472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.style.BackgroundColorSpan; 3515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.util.Log; 3615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.KeyEvent; 3715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.View; 38472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.widget.TextView; 3915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 4015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport com.android.documentsui.Events; 41472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport com.android.documentsui.R; 42472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 43472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.ArrayList; 44472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.List; 456fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.Timer; 466fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.TimerTask; 4715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 4815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa/** 4915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * A class that handles navigation and focus within the DirectoryFragment. 5015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 5174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwaclass FocusManager implements View.OnFocusChangeListener { 5215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private static final String TAG = "FocusManager"; 5315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 5415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private RecyclerView mView; 55472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private DocumentsAdapter mAdapter; 56a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa private GridLayoutManager mLayout; 5715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 58472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private TitleSearchHelper mSearchHelper; 59472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private Model mModel; 60472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 6174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa private int mLastFocusPosition = RecyclerView.NO_POSITION; 6274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 63472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public FocusManager(Context context, RecyclerView view, Model model) { 6415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa mView = view; 65472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mAdapter = (DocumentsAdapter) view.getAdapter(); 66a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa mLayout = (GridLayoutManager) view.getLayoutManager(); 67472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel = model; 68472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 69472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSearchHelper = new TitleSearchHelper(context); 7015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 7115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 7215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 7315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key 7415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * events. 7515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 7615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param doc The DocumentHolder receiving the key event. 7715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode 7815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 7915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return Whether the event was handled. 8015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 8115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 82472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Search helper gets first crack, for doing type-to-focus. 83472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mSearchHelper.handleKey(doc, keyCode, event)) { 84472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 85472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 86472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 8792db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa // Translate space/shift-space into PgDn/PgUp 8892db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa if (keyCode == KeyEvent.KEYCODE_SPACE) { 8992db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa if (event.isShiftPressed()) { 9092db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa keyCode = KeyEvent.KEYCODE_PAGE_UP; 9192db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } else { 9292db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa keyCode = KeyEvent.KEYCODE_PAGE_DOWN; 9392db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } 9492db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } 9592db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa 9615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 9715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find the target item and focus it. 9815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int endPos = findTargetPosition(doc.itemView, keyCode, event); 9915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 10015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (endPos != RecyclerView.NO_POSITION) { 10115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa focusItem(endPos); 10215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 10374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Swallow all navigation keystrokes. Otherwise they go to the app's global 10474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // key-handler, which will route them back to the DF and cause focus to be reset. 10574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return true; 10674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 10774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return false; 10874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 10974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 11074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa @Override 11174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa public void onFocusChange(View v, boolean hasFocus) { 11274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Remember focus events on items. 11374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa if (hasFocus && v.getParent() == mView) { 11474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa mLastFocusPosition = mView.getChildAdapterPosition(v); 11574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 11674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 11774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 11874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa /** 11974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa * Requests focus on the item that last had focus. Scrolls to that item if necessary. 12074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa */ 12174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa public void restoreLastFocus() { 12274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa if (mLastFocusPosition != RecyclerView.NO_POSITION) { 12374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // The system takes care of situations when a view is no longer on screen, etc, 12474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa focusItem(mLastFocusPosition); 12574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } else { 12674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Focus the first visible item 12774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa focusItem(mLayout.findFirstVisibleItemPosition()); 12815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 12915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 13015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 13115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 1329504d764fc5b625661959ed6dcd190b9730d418dBen Kwa * @return The adapter position of the last focused item. 1339504d764fc5b625661959ed6dcd190b9730d418dBen Kwa */ 1349504d764fc5b625661959ed6dcd190b9730d418dBen Kwa public int getFocusPosition() { 1359504d764fc5b625661959ed6dcd190b9730d418dBen Kwa return mLastFocusPosition; 1369504d764fc5b625661959ed6dcd190b9730d418dBen Kwa } 1379504d764fc5b625661959ed6dcd190b9730d418dBen Kwa 1389504d764fc5b625661959ed6dcd190b9730d418dBen Kwa /** 13915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Finds the destination position where the focus should land for a given navigation event. 14015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 14115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the event. 14215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode The key code for the event. 14315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 14415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION. 14515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 14615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findTargetPosition(View view, int keyCode, KeyEvent event) { 14715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 14815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_HOME: 14915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return 0; 15015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_END: 15115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return mAdapter.getItemCount() - 1; 15215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_UP: 15315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_DOWN: 15415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return findPagedTargetPosition(view, keyCode, event); 15515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 15615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 15715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find a navigation target based on the arrow key that the user pressed. 15815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int searchDir = -1; 15915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 16015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_UP: 16115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_UP; 16215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 16315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_DOWN: 16415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_DOWN; 16515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 166a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 167a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 168a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (inGridMode()) { 169a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa int currentPosition = mView.getChildAdapterPosition(view); 170a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Left and right arrow keys only work in grid mode. 171a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa switch (keyCode) { 172a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_LEFT: 173a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (currentPosition > 0) { 174a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop backward focus search at the first item, otherwise focus will wrap 175a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the last visible item. 176a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_BACKWARD; 177a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 178a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 179a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_RIGHT: 180a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (currentPosition < mAdapter.getItemCount() - 1) { 181a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop forward focus search at the last item, otherwise focus will wrap 182a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the first visible item. 183a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_FORWARD; 184a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 185a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 186a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 18715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 18815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 18915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (searchDir != -1) { 19067f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // Focus search behaves badly if the parent RecyclerView is focused. However, focusable 19167f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after 19267f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable 19367f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // off while performing the focus search. 19467f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // TODO: Revisit this when RV focus issues are resolved. 19567f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa mView.setFocusable(false); 19615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa View targetView = view.focusSearch(searchDir); 19767f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa mView.setFocusable(true); 19815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // TargetView can be null, for example, if the user pressed <down> at the bottom 19915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // of the list. 20015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (targetView != null) { 20115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Ignore navigation targets that aren't items in the RecyclerView. 20215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (targetView.getParent() == mView) { 20315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return mView.getChildAdapterPosition(targetView); 20415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 20815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return RecyclerView.NO_POSITION; 20915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 21015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 21115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 21215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Given a PgUp/PgDn event and the current view, find the position of the target view. 21315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * This returns: 21415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The position of the topmost (or bottom-most) visible item, if the current item is not 21515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * the top- or bottom-most visible item. 21615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The position of an item that is one page's worth of items up (or down) if the current 21715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * item is the top- or bottom-most visible item. 21815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The first (or last) item, if paging up (or down) would go past those limits. 21915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the key event. 22015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN. 22115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 22215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the target item. 22315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 22415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) { 22515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int first = mLayout.findFirstVisibleItemPosition(); 22615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int last = mLayout.findLastVisibleItemPosition(); 22715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int current = mView.getChildAdapterPosition(view); 22815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int pageSize = last - first + 1; 22915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 23015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { 23115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current > first) { 23215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the first item, target the first item. 23315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return first; 23415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 23515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the first item, target the item one page up. 23615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current - pageSize; 23715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < 0 ? 0 : target; 23815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 23915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 24015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 24115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) { 24215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current < last) { 24315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the last item, target the last item. 24415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return last; 24515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 24615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the last item, target the item one page down. 24715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current + pageSize; 24815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int max = mAdapter.getItemCount() - 1; 24915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < max ? target : max; 25015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 25115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 25215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 25315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa throw new IllegalArgumentException("Unsupported keyCode: " + keyCode); 25415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 25515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 25615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 25715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 25815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * necessary. 25915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 26015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param pos 26115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 26215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private void focusItem(final int pos) { 2636fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa focusItem(pos, null); 2646fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 2656fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 2666fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa /** 2676fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 2686fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * necessary. 2696fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 2706fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param pos 2716fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param callback A callback to call after the given item has been focused. 2726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa */ 2736fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void focusItem(final int pos, @Nullable final FocusCallback callback) { 27415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the item is already in view, focus it; otherwise, scroll to it and focus it. 27515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); 27615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 2776fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 2786fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa callback.onFocus(vh.itemView); 2796fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 28015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 28115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Set a one-time listener to request focus when the scroll has completed. 28215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa mView.addOnScrollListener( 28315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa new RecyclerView.OnScrollListener() { 28415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa @Override 28515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public void onScrollStateChanged(RecyclerView view, int newState) { 28615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (newState == RecyclerView.SCROLL_STATE_IDLE) { 28715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // When scrolling stops, find the item and focus it. 28815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa RecyclerView.ViewHolder vh = 28915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa view.findViewHolderForAdapterPosition(pos); 29015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 2916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 2926fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa callback.onFocus(vh.itemView); 2936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 29415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 29515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // This might happen in weird corner cases, e.g. if the user is 29615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // scrolling while a delete operation is in progress. In that 29715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // case, just don't attempt to focus the missing item. 29815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa Log.w(TAG, "Unable to focus position " + pos + " after scroll"); 29915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 30015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa view.removeOnScrollListener(this); 30115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 30215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 30315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa }); 304472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mView.smoothScrollToPosition(pos); 30515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 30615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 307a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 308a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa /** 309a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa * @return Whether the layout manager is currently in a grid-configuration. 310a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa */ 311a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa private boolean inGridMode() { 312a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa return mLayout.getSpanCount() > 1; 313a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 314472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 3156fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private interface FocusCallback { 3166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void onFocus(View view); 3176fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 3186fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 319472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 320472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via 321472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build 322472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * up a string from individual key events, and perform searching based on that string. When an 323472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * item is found that matches the search term, that item will be focused. This class also 324472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * highlights instances of the search term found in the view. 325472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 326472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private class TitleSearchHelper { 3276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa static private final int SEARCH_TIMEOUT = 500; // ms 3286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 3296fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false); 3306fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final Editable mSearchString = Editable.Factory.getInstance().newEditable(""); 3316fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final Highlighter mHighlighter = new Highlighter(); 3326fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final BackgroundColorSpan mSpan; 3336fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 334472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private List<String> mIndex; 335472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private boolean mActive; 3366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Timer mTimer; 3376fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private KeyEvent mLastEvent; 3386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Handler mUiRunner; 339472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 340472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public TitleSearchHelper(Context context) { 341472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSpan = new BackgroundColorSpan(context.getColor(R.color.accent_dark)); 3426fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Handler for running things on the main UI thread. Needed for updating the UI from a 3436fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // timer (see #activate, below). 3446fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mUiRunner = new Handler(Looper.getMainLooper()); 345472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 346472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 347472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 348472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out 349472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * of individual key events, and then performs a search for the given string. 350472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * 351472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param doc The document holder receiving the key event. 352472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param keyCode 353472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param event 354472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @return Whether the event was handled. 355472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 356472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 357472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa switch (keyCode) { 358472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ESCAPE: 359472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ENTER: 360472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 361472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // These keys end any active searches. 3626fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 363472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 364472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 365472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle these key events if there is no active search. 366472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 367472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 368472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_SPACE: 369472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // This allows users to search for files with spaces in their names, but ignores 3706fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // spacebar events when a text search is not active. Ignoring the spacebar 3716fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // event is necessary because other handlers (see FocusManager#handleKey) also 3726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // listen for and handle it. 373472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 374472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 375472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 376472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 377472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 378472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Navigation keys also end active searches. 379472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 3806fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 381472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle the keycode, so navigation still occurs. 382472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 383472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 384472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 385472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Build up the search string, and perform the search. 386472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event); 387472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 388472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Delete is processed by the text listener, but not "handled". Check separately for it. 3896fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (keyCode == KeyEvent.KEYCODE_DEL) { 3906fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa handled = true; 3916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 3926fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 3936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (handled) { 3946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mLastEvent = event; 3956fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mSearchString.length() == 0) { 396472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't perform empty searches. 397472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 398472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 3996fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa search(); 400472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 401472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 402472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return handled; 403472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 404472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 405472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 406472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Activates the search helper, which changes its key handling and updates the search index 407472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * and highlights if necessary. Call this each time the search term is updated. 408472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 4096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void search() { 410472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 4116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // The model listener invalidates the search index when the model changes. 412472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel.addUpdateListener(mModelListener); 4136fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 4146fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Used to keep the current search alive until the timeout expires. If the user 4156fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // presses another key within that time, that keystroke is added to the current 4166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // search. Otherwise, the current search ends, and subsequent keystrokes start a new 4176fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // search. 4186fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer = new Timer(); 4196fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mActive = true; 420472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 421472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 422472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // If the search index was invalidated, rebuild it 423472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mIndex == null) { 424472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa buildIndex(); 425472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 426472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 4276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Search for the current search term. 4286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Perform case-insensitive search. 4296fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String searchString = mSearchString.toString().toLowerCase(); 4306fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa for (int pos = 0; pos < mIndex.size(); pos++) { 4316fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String title = mIndex.get(pos); 4326fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (title != null && title.startsWith(searchString)) { 4336fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa focusItem(pos, new FocusCallback() { 4346fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa @Override 4356fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void onFocus(View view) { 4366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mHighlighter.applyHighlight(view); 4376fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Using a timer repeat period of SEARCH_TIMEOUT/2 means the amount of 4386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // time between the last keystroke and a search expiring is actually 4396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // between 500 and 750 ms. A smaller timer period results in less 4406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // variability but does more polling. 4416fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer.schedule(new TimeoutTask(), 0, SEARCH_TIMEOUT / 2); 4426fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 4436fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa }); 4446fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa break; 4456fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 4466fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 447472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 448472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 449472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 4506fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Ends the current search (see {@link #search()}. 451472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 4526fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void endSearch() { 453472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 454472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel.removeUpdateListener(mModelListener); 4556fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer.cancel(); 456472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 457472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 4586fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mHighlighter.removeHighlight(); 459472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 460472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 461472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSearchString.clear(); 462472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mActive = false; 463472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 464472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 465472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 466472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Builds a search index for finding items by title. Queries the model and adapter, so both 467472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * must be set up before calling this method. 468472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 469472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void buildIndex() { 470472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa int itemCount = mAdapter.getItemCount(); 471472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa List<String> index = new ArrayList<>(itemCount); 472472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa for (int i = 0; i < itemCount; i++) { 473472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa String modelId = mAdapter.getModelId(i); 474472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (modelId != null) { 4756fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String title = 4766fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa getCursorString(mModel.getItem(modelId), Document.COLUMN_DISPLAY_NAME); 4776fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Perform case-insensitive search. 4786fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa index.add(title.toLowerCase()); 479472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 480472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa index.add(""); 481472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 482472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 483472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = index; 484472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 485472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 486472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private Model.UpdateListener mModelListener = new Model.UpdateListener() { 487472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 488472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void onModelUpdate(Model model) { 489472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Invalidate the search index when the model updates. 490472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 491472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 492472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 493472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 494472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void onModelUpdateFailed(Exception e) { 495472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Invalidate the search index when the model updates. 496472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 497472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 498472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 499472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5006fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private class TimeoutTask extends TimerTask { 5016fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa @Override 5026fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void run() { 5036fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa long last = mLastEvent.getEventTime(); 5046fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa long now = SystemClock.uptimeMillis(); 5056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if ((now - last) > SEARCH_TIMEOUT) { 5066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // endSearch must run on the main thread because it does UI work 5076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mUiRunner.post(new Runnable() { 5086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa @Override 5096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void run() { 5106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 5116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 5126fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa }); 513472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 514472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 5156fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa }; 5166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 5176fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private class Highlighter { 5186fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Spannable mCurrentHighlight; 519472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 520472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 5216fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Applies title highlights to the given view. The view must have a title field that is a 5226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * spannable text field. If this condition is not met, this function does nothing. 5236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 5246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param view 525472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 5266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void applyHighlight(View view) { 5276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa TextView titleView = (TextView) view.findViewById(android.R.id.title); 5286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (titleView == null) { 5296fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa return; 530472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 531472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5326fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa CharSequence tmpText = titleView.getText(); 5336fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (tmpText instanceof Spannable) { 5346fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mCurrentHighlight != null) { 5356fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.removeSpan(mSpan); 5366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 5376fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight = (Spannable) tmpText; 5386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.setSpan( 5396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mSpan, 0, mSearchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 5406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 541472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 542472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5436fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa /** 5446fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Removes title highlights from the given view. The view must have a title field that is a 5456fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * spannable text field. If this condition is not met, this function does nothing. 5466fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 5476fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param view 5486fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa */ 5496fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void removeHighlight() { 5506fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mCurrentHighlight != null) { 5516fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.removeSpan(mSpan); 552472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 553472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 554472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 555472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 55615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa} 557