1b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa/* 2b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * Copyright (C) 2016 The Android Open Source Project 3b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * 4b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * Licensed under the Apache License, Version 2.0 (the "License"); 5b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * you may not use this file except in compliance with the License. 6b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * You may obtain a copy of the License at 7b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * 8b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * http://www.apache.org/licenses/LICENSE-2.0 9b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * 10b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * Unless required by applicable law or agreed to in writing, software 11b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * distributed under the License is distributed on an "AS IS" BASIS, 12b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * See the License for the specific language governing permissions and 14b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * limitations under the License. 15b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa */ 16b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 17b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwapackage com.android.documentsui.dirlist; 18b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 19a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport static com.android.documentsui.model.DocumentInfo.getCursorString; 20a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 2122937c9fc71beb045384417fe226504b1f003470Ben Kwaimport android.annotation.Nullable; 22a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.content.Context; 23dcc68fdd0ca1f0d2d2dfb979dd837ac2dd2e16f3Steve McKayimport android.database.Cursor; 2422937c9fc71beb045384417fe226504b1f003470Ben Kwaimport android.os.Handler; 2522937c9fc71beb045384417fe226504b1f003470Ben Kwaimport android.os.Looper; 2622937c9fc71beb045384417fe226504b1f003470Ben Kwaimport android.os.SystemClock; 27a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.provider.DocumentsContract.Document; 28d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwaimport android.support.v7.widget.GridLayoutManager; 29b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwaimport android.support.v7.widget.RecyclerView; 30a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.text.Editable; 31a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.text.Spannable; 32a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.text.method.KeyListener; 33a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.text.method.TextKeyListener; 34a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.text.method.TextKeyListener.Capitalize; 35a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.text.style.BackgroundColorSpan; 36b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwaimport android.util.Log; 37b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwaimport android.view.KeyEvent; 38b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwaimport android.view.View; 39a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport android.widget.TextView; 40b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 41b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwaimport com.android.documentsui.Events; 42a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport com.android.documentsui.R; 43a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 44a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport java.util.ArrayList; 45a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwaimport java.util.List; 4622937c9fc71beb045384417fe226504b1f003470Ben Kwaimport java.util.Timer; 4722937c9fc71beb045384417fe226504b1f003470Ben Kwaimport java.util.TimerTask; 48b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 49b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa/** 50b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * A class that handles navigation and focus within the DirectoryFragment. 51b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa */ 522036dad877336bf4d81202c334b1f2c81462bd9fBen Kwaclass FocusManager implements View.OnFocusChangeListener { 53b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa private static final String TAG = "FocusManager"; 54b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 55b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa private RecyclerView mView; 56a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa private DocumentsAdapter mAdapter; 57d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa private GridLayoutManager mLayout; 58b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 59a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa private TitleSearchHelper mSearchHelper; 60a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa private Model mModel; 61a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 622036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa private int mLastFocusPosition = RecyclerView.NO_POSITION; 632036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa 64a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa public FocusManager(Context context, RecyclerView view, Model model) { 65b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa mView = view; 66a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mAdapter = (DocumentsAdapter) view.getAdapter(); 67d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa mLayout = (GridLayoutManager) view.getLayoutManager(); 68a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mModel = model; 69a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 70a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mSearchHelper = new TitleSearchHelper(context); 71b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 72b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 73b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa /** 74b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key 75b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * events. 76b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * 77b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param doc The DocumentHolder receiving the key event. 78b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param keyCode 79b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param event 80b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @return Whether the event was handled. 81b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa */ 82b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 83a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Search helper gets first crack, for doing type-to-focus. 84a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa if (mSearchHelper.handleKey(doc, keyCode, event)) { 85a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa return true; 86a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 87a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 88c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa // Translate space/shift-space into PgDn/PgUp 89c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa if (keyCode == KeyEvent.KEYCODE_SPACE) { 90c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa if (event.isShiftPressed()) { 91c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa keyCode = KeyEvent.KEYCODE_PAGE_UP; 92c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa } else { 93c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa keyCode = KeyEvent.KEYCODE_PAGE_DOWN; 94c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa } 95c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa } 96c345079b74f74531767240dbfa6cf049b2861a2dBen Kwa 97b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (Events.isNavigationKeyCode(keyCode)) { 98b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // Find the target item and focus it. 99b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int endPos = findTargetPosition(doc.itemView, keyCode, event); 100b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 101b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (endPos != RecyclerView.NO_POSITION) { 102b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa focusItem(endPos); 103b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 1042036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa // Swallow all navigation keystrokes. Otherwise they go to the app's global 1052036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa // key-handler, which will route them back to the DF and cause focus to be reset. 1062036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa return true; 1072036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa } 1082036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa return false; 1092036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa } 1102036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa 1112036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa @Override 1122036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa public void onFocusChange(View v, boolean hasFocus) { 1132036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa // Remember focus events on items. 1142036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa if (hasFocus && v.getParent() == mView) { 1152036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa mLastFocusPosition = mView.getChildAdapterPosition(v); 1162036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa } 1172036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa } 1182036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa 1192036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa /** 1202036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa * Requests focus on the item that last had focus. Scrolls to that item if necessary. 1212036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa */ 1222036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa public void restoreLastFocus() { 1239b3524d8397ff2bebf64331063a14a40b61924f4Ben Kwa if (mAdapter.getItemCount() == 0) { 1249b3524d8397ff2bebf64331063a14a40b61924f4Ben Kwa // Nothing to focus. 1259b3524d8397ff2bebf64331063a14a40b61924f4Ben Kwa return; 1269b3524d8397ff2bebf64331063a14a40b61924f4Ben Kwa } 1279b3524d8397ff2bebf64331063a14a40b61924f4Ben Kwa 1282036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa if (mLastFocusPosition != RecyclerView.NO_POSITION) { 1292036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa // The system takes care of situations when a view is no longer on screen, etc, 1302036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa focusItem(mLastFocusPosition); 1312036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa } else { 1322036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa // Focus the first visible item 1332036dad877336bf4d81202c334b1f2c81462bd9fBen Kwa focusItem(mLayout.findFirstVisibleItemPosition()); 134b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 135b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 136b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 137b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa /** 13883df50f9971d79fcffe78a9ea1a9eeebcea996bcBen Kwa * @return The adapter position of the last focused item. 13983df50f9971d79fcffe78a9ea1a9eeebcea996bcBen Kwa */ 14083df50f9971d79fcffe78a9ea1a9eeebcea996bcBen Kwa public int getFocusPosition() { 14183df50f9971d79fcffe78a9ea1a9eeebcea996bcBen Kwa return mLastFocusPosition; 14283df50f9971d79fcffe78a9ea1a9eeebcea996bcBen Kwa } 14383df50f9971d79fcffe78a9ea1a9eeebcea996bcBen Kwa 14483df50f9971d79fcffe78a9ea1a9eeebcea996bcBen Kwa /** 145b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * Finds the destination position where the focus should land for a given navigation event. 146b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * 147b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param view The view that received the event. 148b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param keyCode The key code for the event. 149b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param event 150b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION. 151b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa */ 152b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa private int findTargetPosition(View view, int keyCode, KeyEvent event) { 153b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa switch (keyCode) { 154b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa case KeyEvent.KEYCODE_MOVE_HOME: 155b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return 0; 156b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa case KeyEvent.KEYCODE_MOVE_END: 157b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return mAdapter.getItemCount() - 1; 158b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa case KeyEvent.KEYCODE_PAGE_UP: 159b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa case KeyEvent.KEYCODE_PAGE_DOWN: 160b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return findPagedTargetPosition(view, keyCode, event); 161b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 162b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 163b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // Find a navigation target based on the arrow key that the user pressed. 164b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int searchDir = -1; 165b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa switch (keyCode) { 166b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa case KeyEvent.KEYCODE_DPAD_UP: 167b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa searchDir = View.FOCUS_UP; 168b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa break; 169b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa case KeyEvent.KEYCODE_DPAD_DOWN: 170b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa searchDir = View.FOCUS_DOWN; 171b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa break; 172d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa } 173d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa 174d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa if (inGridMode()) { 175d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa int currentPosition = mView.getChildAdapterPosition(view); 176d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa // Left and right arrow keys only work in grid mode. 177d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa switch (keyCode) { 178d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa case KeyEvent.KEYCODE_DPAD_LEFT: 179d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa if (currentPosition > 0) { 180d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa // Stop backward focus search at the first item, otherwise focus will wrap 181d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa // around to the last visible item. 182d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa searchDir = View.FOCUS_BACKWARD; 183d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa } 184d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa break; 185d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa case KeyEvent.KEYCODE_DPAD_RIGHT: 186d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa if (currentPosition < mAdapter.getItemCount() - 1) { 187d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa // Stop forward focus search at the last item, otherwise focus will wrap 188d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa // around to the first visible item. 189d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa searchDir = View.FOCUS_FORWARD; 190d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa } 191d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa break; 192d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa } 193b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 194b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 195b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (searchDir != -1) { 19686bf12397bd0073f0acee9bb1c8b4099eb40b31fBen Kwa // Focus search behaves badly if the parent RecyclerView is focused. However, focusable 19786bf12397bd0073f0acee9bb1c8b4099eb40b31fBen Kwa // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after 19886bf12397bd0073f0acee9bb1c8b4099eb40b31fBen Kwa // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable 19986bf12397bd0073f0acee9bb1c8b4099eb40b31fBen Kwa // off while performing the focus search. 20086bf12397bd0073f0acee9bb1c8b4099eb40b31fBen Kwa // TODO: Revisit this when RV focus issues are resolved. 20186bf12397bd0073f0acee9bb1c8b4099eb40b31fBen Kwa mView.setFocusable(false); 202b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa View targetView = view.focusSearch(searchDir); 20386bf12397bd0073f0acee9bb1c8b4099eb40b31fBen Kwa mView.setFocusable(true); 204b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // TargetView can be null, for example, if the user pressed <down> at the bottom 205b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // of the list. 206b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (targetView != null) { 207b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // Ignore navigation targets that aren't items in the RecyclerView. 208b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (targetView.getParent() == mView) { 209b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return mView.getChildAdapterPosition(targetView); 210b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 211b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 212b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 213b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 214b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return RecyclerView.NO_POSITION; 215b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 216b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 217b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa /** 218b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * Given a PgUp/PgDn event and the current view, find the position of the target view. 219b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * This returns: 220b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * <li>The position of the topmost (or bottom-most) visible item, if the current item is not 221b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * the top- or bottom-most visible item. 222b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * <li>The position of an item that is one page's worth of items up (or down) if the current 223b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * item is the top- or bottom-most visible item. 224b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * <li>The first (or last) item, if paging up (or down) would go past those limits. 225b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param view The view that received the key event. 226b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN. 227b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param event 228b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @return The adapter position of the target item. 229b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa */ 230b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) { 231b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int first = mLayout.findFirstVisibleItemPosition(); 232b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int last = mLayout.findLastVisibleItemPosition(); 233b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int current = mView.getChildAdapterPosition(view); 234b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int pageSize = last - first + 1; 235b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 236b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { 237b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (current > first) { 238b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // If the current item isn't the first item, target the first item. 239b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return first; 240b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } else { 241b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // If the current item is the first item, target the item one page up. 242b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int target = current - pageSize; 243b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return target < 0 ? 0 : target; 244b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 245b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 246b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 247b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) { 248b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (current < last) { 249b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // If the current item isn't the last item, target the last item. 250b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return last; 251b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } else { 252b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // If the current item is the last item, target the item one page down. 253b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int target = current + pageSize; 254b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa int max = mAdapter.getItemCount() - 1; 255b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa return target < max ? target : max; 256b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 257b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 258b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 259b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa throw new IllegalArgumentException("Unsupported keyCode: " + keyCode); 260b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 261b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa 262b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa /** 263b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 264b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * necessary. 265b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * 266b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa * @param pos 267b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa */ 268b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa private void focusItem(final int pos) { 26922937c9fc71beb045384417fe226504b1f003470Ben Kwa focusItem(pos, null); 27022937c9fc71beb045384417fe226504b1f003470Ben Kwa } 27122937c9fc71beb045384417fe226504b1f003470Ben Kwa 27222937c9fc71beb045384417fe226504b1f003470Ben Kwa /** 27322937c9fc71beb045384417fe226504b1f003470Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 27422937c9fc71beb045384417fe226504b1f003470Ben Kwa * necessary. 27522937c9fc71beb045384417fe226504b1f003470Ben Kwa * 27622937c9fc71beb045384417fe226504b1f003470Ben Kwa * @param pos 27722937c9fc71beb045384417fe226504b1f003470Ben Kwa * @param callback A callback to call after the given item has been focused. 27822937c9fc71beb045384417fe226504b1f003470Ben Kwa */ 27922937c9fc71beb045384417fe226504b1f003470Ben Kwa private void focusItem(final int pos, @Nullable final FocusCallback callback) { 280b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // If the item is already in view, focus it; otherwise, scroll to it and focus it. 281b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); 282b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (vh != null) { 28322937c9fc71beb045384417fe226504b1f003470Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 28422937c9fc71beb045384417fe226504b1f003470Ben Kwa callback.onFocus(vh.itemView); 28522937c9fc71beb045384417fe226504b1f003470Ben Kwa } 286b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } else { 287b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // Set a one-time listener to request focus when the scroll has completed. 288b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa mView.addOnScrollListener( 289b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa new RecyclerView.OnScrollListener() { 290b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa @Override 291b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa public void onScrollStateChanged(RecyclerView view, int newState) { 292b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (newState == RecyclerView.SCROLL_STATE_IDLE) { 293b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // When scrolling stops, find the item and focus it. 294b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa RecyclerView.ViewHolder vh = 295b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa view.findViewHolderForAdapterPosition(pos); 296b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa if (vh != null) { 29722937c9fc71beb045384417fe226504b1f003470Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 29822937c9fc71beb045384417fe226504b1f003470Ben Kwa callback.onFocus(vh.itemView); 29922937c9fc71beb045384417fe226504b1f003470Ben Kwa } 300b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } else { 301b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // This might happen in weird corner cases, e.g. if the user is 302b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // scrolling while a delete operation is in progress. In that 303b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa // case, just don't attempt to focus the missing item. 304b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa Log.w(TAG, "Unable to focus position " + pos + " after scroll"); 305b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 306b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa view.removeOnScrollListener(this); 307b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 308b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 309b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa }); 310a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mView.smoothScrollToPosition(pos); 311b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 312b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa } 313d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa 314d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa /** 315d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa * @return Whether the layout manager is currently in a grid-configuration. 316d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa */ 317d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa private boolean inGridMode() { 318d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa return mLayout.getSpanCount() > 1; 319d7211c85d676dbb6071bb765c352c748f08d5458Ben Kwa } 320a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 32122937c9fc71beb045384417fe226504b1f003470Ben Kwa private interface FocusCallback { 32222937c9fc71beb045384417fe226504b1f003470Ben Kwa public void onFocus(View view); 32322937c9fc71beb045384417fe226504b1f003470Ben Kwa } 32422937c9fc71beb045384417fe226504b1f003470Ben Kwa 325a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa /** 326a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via 327a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build 328a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * up a string from individual key events, and perform searching based on that string. When an 329a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * item is found that matches the search term, that item will be focused. This class also 330a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * highlights instances of the search term found in the view. 331a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa */ 332a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa private class TitleSearchHelper { 33322937c9fc71beb045384417fe226504b1f003470Ben Kwa static private final int SEARCH_TIMEOUT = 500; // ms 33422937c9fc71beb045384417fe226504b1f003470Ben Kwa 33522937c9fc71beb045384417fe226504b1f003470Ben Kwa private final KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false); 33622937c9fc71beb045384417fe226504b1f003470Ben Kwa private final Editable mSearchString = Editable.Factory.getInstance().newEditable(""); 33722937c9fc71beb045384417fe226504b1f003470Ben Kwa private final Highlighter mHighlighter = new Highlighter(); 33822937c9fc71beb045384417fe226504b1f003470Ben Kwa private final BackgroundColorSpan mSpan; 33922937c9fc71beb045384417fe226504b1f003470Ben Kwa 340a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa private List<String> mIndex; 341a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa private boolean mActive; 34222937c9fc71beb045384417fe226504b1f003470Ben Kwa private Timer mTimer; 34322937c9fc71beb045384417fe226504b1f003470Ben Kwa private KeyEvent mLastEvent; 34422937c9fc71beb045384417fe226504b1f003470Ben Kwa private Handler mUiRunner; 345a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 346a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa public TitleSearchHelper(Context context) { 347a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mSpan = new BackgroundColorSpan(context.getColor(R.color.accent_dark)); 34822937c9fc71beb045384417fe226504b1f003470Ben Kwa // Handler for running things on the main UI thread. Needed for updating the UI from a 34922937c9fc71beb045384417fe226504b1f003470Ben Kwa // timer (see #activate, below). 35022937c9fc71beb045384417fe226504b1f003470Ben Kwa mUiRunner = new Handler(Looper.getMainLooper()); 351a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 352a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 353a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa /** 354a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out 355a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * of individual key events, and then performs a search for the given string. 356a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * 357a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * @param doc The document holder receiving the key event. 358a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * @param keyCode 359a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * @param event 360a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * @return Whether the event was handled. 361a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa */ 362a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 363a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa switch (keyCode) { 364a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa case KeyEvent.KEYCODE_ESCAPE: 365a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa case KeyEvent.KEYCODE_ENTER: 366a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa if (mActive) { 367a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // These keys end any active searches. 36822937c9fc71beb045384417fe226504b1f003470Ben Kwa endSearch(); 369a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa return true; 370a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } else { 371a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Don't handle these key events if there is no active search. 372a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa return false; 373a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 374a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa case KeyEvent.KEYCODE_SPACE: 375a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // This allows users to search for files with spaces in their names, but ignores 37622937c9fc71beb045384417fe226504b1f003470Ben Kwa // spacebar events when a text search is not active. Ignoring the spacebar 37722937c9fc71beb045384417fe226504b1f003470Ben Kwa // event is necessary because other handlers (see FocusManager#handleKey) also 37822937c9fc71beb045384417fe226504b1f003470Ben Kwa // listen for and handle it. 379a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa if (!mActive) { 380a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa return false; 381a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 382a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 383a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 384a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Navigation keys also end active searches. 385a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 38622937c9fc71beb045384417fe226504b1f003470Ben Kwa endSearch(); 387a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Don't handle the keycode, so navigation still occurs. 388a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa return false; 389a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 390a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 391a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Build up the search string, and perform the search. 392a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event); 393a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 394a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Delete is processed by the text listener, but not "handled". Check separately for it. 39522937c9fc71beb045384417fe226504b1f003470Ben Kwa if (keyCode == KeyEvent.KEYCODE_DEL) { 39622937c9fc71beb045384417fe226504b1f003470Ben Kwa handled = true; 39722937c9fc71beb045384417fe226504b1f003470Ben Kwa } 39822937c9fc71beb045384417fe226504b1f003470Ben Kwa 39922937c9fc71beb045384417fe226504b1f003470Ben Kwa if (handled) { 40022937c9fc71beb045384417fe226504b1f003470Ben Kwa mLastEvent = event; 40122937c9fc71beb045384417fe226504b1f003470Ben Kwa if (mSearchString.length() == 0) { 402a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Don't perform empty searches. 403a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa return false; 404a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 40522937c9fc71beb045384417fe226504b1f003470Ben Kwa search(); 406a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 407a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 408a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa return handled; 409a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 410a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 411a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa /** 412a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * Activates the search helper, which changes its key handling and updates the search index 413a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * and highlights if necessary. Call this each time the search term is updated. 414a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa */ 41522937c9fc71beb045384417fe226504b1f003470Ben Kwa private void search() { 416a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa if (!mActive) { 41722937c9fc71beb045384417fe226504b1f003470Ben Kwa // The model listener invalidates the search index when the model changes. 418a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mModel.addUpdateListener(mModelListener); 41922937c9fc71beb045384417fe226504b1f003470Ben Kwa 42022937c9fc71beb045384417fe226504b1f003470Ben Kwa // Used to keep the current search alive until the timeout expires. If the user 42122937c9fc71beb045384417fe226504b1f003470Ben Kwa // presses another key within that time, that keystroke is added to the current 42222937c9fc71beb045384417fe226504b1f003470Ben Kwa // search. Otherwise, the current search ends, and subsequent keystrokes start a new 42322937c9fc71beb045384417fe226504b1f003470Ben Kwa // search. 42422937c9fc71beb045384417fe226504b1f003470Ben Kwa mTimer = new Timer(); 42522937c9fc71beb045384417fe226504b1f003470Ben Kwa mActive = true; 426a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 427a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 428a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // If the search index was invalidated, rebuild it 429a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa if (mIndex == null) { 430a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa buildIndex(); 431a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 432a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 43322937c9fc71beb045384417fe226504b1f003470Ben Kwa // Search for the current search term. 43422937c9fc71beb045384417fe226504b1f003470Ben Kwa // Perform case-insensitive search. 43522937c9fc71beb045384417fe226504b1f003470Ben Kwa String searchString = mSearchString.toString().toLowerCase(); 43622937c9fc71beb045384417fe226504b1f003470Ben Kwa for (int pos = 0; pos < mIndex.size(); pos++) { 43722937c9fc71beb045384417fe226504b1f003470Ben Kwa String title = mIndex.get(pos); 43822937c9fc71beb045384417fe226504b1f003470Ben Kwa if (title != null && title.startsWith(searchString)) { 43922937c9fc71beb045384417fe226504b1f003470Ben Kwa focusItem(pos, new FocusCallback() { 44022937c9fc71beb045384417fe226504b1f003470Ben Kwa @Override 44122937c9fc71beb045384417fe226504b1f003470Ben Kwa public void onFocus(View view) { 44222937c9fc71beb045384417fe226504b1f003470Ben Kwa mHighlighter.applyHighlight(view); 44322937c9fc71beb045384417fe226504b1f003470Ben Kwa // Using a timer repeat period of SEARCH_TIMEOUT/2 means the amount of 44422937c9fc71beb045384417fe226504b1f003470Ben Kwa // time between the last keystroke and a search expiring is actually 44522937c9fc71beb045384417fe226504b1f003470Ben Kwa // between 500 and 750 ms. A smaller timer period results in less 44622937c9fc71beb045384417fe226504b1f003470Ben Kwa // variability but does more polling. 44722937c9fc71beb045384417fe226504b1f003470Ben Kwa mTimer.schedule(new TimeoutTask(), 0, SEARCH_TIMEOUT / 2); 44822937c9fc71beb045384417fe226504b1f003470Ben Kwa } 44922937c9fc71beb045384417fe226504b1f003470Ben Kwa }); 45022937c9fc71beb045384417fe226504b1f003470Ben Kwa break; 45122937c9fc71beb045384417fe226504b1f003470Ben Kwa } 45222937c9fc71beb045384417fe226504b1f003470Ben Kwa } 453a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 454a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 455a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa /** 45622937c9fc71beb045384417fe226504b1f003470Ben Kwa * Ends the current search (see {@link #search()}. 457a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa */ 45822937c9fc71beb045384417fe226504b1f003470Ben Kwa private void endSearch() { 459a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa if (mActive) { 460a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mModel.removeUpdateListener(mModelListener); 46122937c9fc71beb045384417fe226504b1f003470Ben Kwa mTimer.cancel(); 462a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 463a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 46422937c9fc71beb045384417fe226504b1f003470Ben Kwa mHighlighter.removeHighlight(); 465a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 466a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mIndex = null; 467a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mSearchString.clear(); 468a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mActive = false; 469a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 470a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 471a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa /** 472a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * Builds a search index for finding items by title. Queries the model and adapter, so both 473a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa * must be set up before calling this method. 474a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa */ 475a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa private void buildIndex() { 476a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa int itemCount = mAdapter.getItemCount(); 477a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa List<String> index = new ArrayList<>(itemCount); 478a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa for (int i = 0; i < itemCount; i++) { 479a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa String modelId = mAdapter.getModelId(i); 480dcc68fdd0ca1f0d2d2dfb979dd837ac2dd2e16f3Steve McKay Cursor cursor = mModel.getItem(modelId); 481dcc68fdd0ca1f0d2d2dfb979dd837ac2dd2e16f3Steve McKay if (modelId != null && cursor != null) { 482dcc68fdd0ca1f0d2d2dfb979dd837ac2dd2e16f3Steve McKay String title = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 48322937c9fc71beb045384417fe226504b1f003470Ben Kwa // Perform case-insensitive search. 48422937c9fc71beb045384417fe226504b1f003470Ben Kwa index.add(title.toLowerCase()); 485a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } else { 486a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa index.add(""); 487a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 488a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 489a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mIndex = index; 490a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 491a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 492a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa private Model.UpdateListener mModelListener = new Model.UpdateListener() { 493a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa @Override 494a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa public void onModelUpdate(Model model) { 495a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Invalidate the search index when the model updates. 496a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mIndex = null; 497a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 498a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 499a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa @Override 500a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa public void onModelUpdateFailed(Exception e) { 501a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa // Invalidate the search index when the model updates. 502a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa mIndex = null; 503a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 504a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa }; 505a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 50622937c9fc71beb045384417fe226504b1f003470Ben Kwa private class TimeoutTask extends TimerTask { 50722937c9fc71beb045384417fe226504b1f003470Ben Kwa @Override 50822937c9fc71beb045384417fe226504b1f003470Ben Kwa public void run() { 50922937c9fc71beb045384417fe226504b1f003470Ben Kwa long last = mLastEvent.getEventTime(); 51022937c9fc71beb045384417fe226504b1f003470Ben Kwa long now = SystemClock.uptimeMillis(); 51122937c9fc71beb045384417fe226504b1f003470Ben Kwa if ((now - last) > SEARCH_TIMEOUT) { 51222937c9fc71beb045384417fe226504b1f003470Ben Kwa // endSearch must run on the main thread because it does UI work 51322937c9fc71beb045384417fe226504b1f003470Ben Kwa mUiRunner.post(new Runnable() { 51422937c9fc71beb045384417fe226504b1f003470Ben Kwa @Override 51522937c9fc71beb045384417fe226504b1f003470Ben Kwa public void run() { 51622937c9fc71beb045384417fe226504b1f003470Ben Kwa endSearch(); 51722937c9fc71beb045384417fe226504b1f003470Ben Kwa } 51822937c9fc71beb045384417fe226504b1f003470Ben Kwa }); 519a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 520a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 52122937c9fc71beb045384417fe226504b1f003470Ben Kwa }; 52222937c9fc71beb045384417fe226504b1f003470Ben Kwa 52322937c9fc71beb045384417fe226504b1f003470Ben Kwa private class Highlighter { 52422937c9fc71beb045384417fe226504b1f003470Ben Kwa private Spannable mCurrentHighlight; 525a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 526a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa /** 52722937c9fc71beb045384417fe226504b1f003470Ben Kwa * Applies title highlights to the given view. The view must have a title field that is a 52822937c9fc71beb045384417fe226504b1f003470Ben Kwa * spannable text field. If this condition is not met, this function does nothing. 52922937c9fc71beb045384417fe226504b1f003470Ben Kwa * 53022937c9fc71beb045384417fe226504b1f003470Ben Kwa * @param view 531a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa */ 53222937c9fc71beb045384417fe226504b1f003470Ben Kwa private void applyHighlight(View view) { 53322937c9fc71beb045384417fe226504b1f003470Ben Kwa TextView titleView = (TextView) view.findViewById(android.R.id.title); 53422937c9fc71beb045384417fe226504b1f003470Ben Kwa if (titleView == null) { 53522937c9fc71beb045384417fe226504b1f003470Ben Kwa return; 536a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 537a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 53822937c9fc71beb045384417fe226504b1f003470Ben Kwa CharSequence tmpText = titleView.getText(); 53922937c9fc71beb045384417fe226504b1f003470Ben Kwa if (tmpText instanceof Spannable) { 54022937c9fc71beb045384417fe226504b1f003470Ben Kwa if (mCurrentHighlight != null) { 54122937c9fc71beb045384417fe226504b1f003470Ben Kwa mCurrentHighlight.removeSpan(mSpan); 54222937c9fc71beb045384417fe226504b1f003470Ben Kwa } 54322937c9fc71beb045384417fe226504b1f003470Ben Kwa mCurrentHighlight = (Spannable) tmpText; 54422937c9fc71beb045384417fe226504b1f003470Ben Kwa mCurrentHighlight.setSpan( 54522937c9fc71beb045384417fe226504b1f003470Ben Kwa mSpan, 0, mSearchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 54622937c9fc71beb045384417fe226504b1f003470Ben Kwa } 547a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 548a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa 54922937c9fc71beb045384417fe226504b1f003470Ben Kwa /** 55022937c9fc71beb045384417fe226504b1f003470Ben Kwa * Removes title highlights from the given view. The view must have a title field that is a 55122937c9fc71beb045384417fe226504b1f003470Ben Kwa * spannable text field. If this condition is not met, this function does nothing. 55222937c9fc71beb045384417fe226504b1f003470Ben Kwa * 55322937c9fc71beb045384417fe226504b1f003470Ben Kwa * @param view 55422937c9fc71beb045384417fe226504b1f003470Ben Kwa */ 55522937c9fc71beb045384417fe226504b1f003470Ben Kwa private void removeHighlight() { 55622937c9fc71beb045384417fe226504b1f003470Ben Kwa if (mCurrentHighlight != null) { 55722937c9fc71beb045384417fe226504b1f003470Ben Kwa mCurrentHighlight.removeSpan(mSpan); 558a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 559a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 560a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa }; 561a4acc90b0a0bbfa55ed247a3ae04c766c885d220Ben Kwa } 562b0bfe2df6f180dc944ccb33695cbea6b45c487dbBen Kwa} 563