FocusManager.java revision 74956af50b13b5ffde252a13547c960ba3e9c5b4
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; 235a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKayimport android.database.Cursor; 246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Handler; 256fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Looper; 266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.SystemClock; 27472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.provider.DocumentsContract.Document; 28a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwaimport android.support.v7.widget.GridLayoutManager; 2915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.support.v7.widget.RecyclerView; 30472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Editable; 31472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Spannable; 32472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.KeyListener; 33472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener; 34472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener.Capitalize; 35472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.style.BackgroundColorSpan; 3615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.util.Log; 3715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.KeyEvent; 3815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.View; 39472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.widget.TextView; 4015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 4115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport com.android.documentsui.Events; 42472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport com.android.documentsui.R; 43472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 44472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.ArrayList; 45472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.List; 466fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.Timer; 476fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.TimerTask; 4815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 4915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa/** 5015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * A class that handles navigation and focus within the DirectoryFragment. 5115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 5274956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKayfinal class FocusManager implements FocusHandler { 5315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private static final String TAG = "FocusManager"; 5415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 5515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private RecyclerView mView; 56472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private DocumentsAdapter mAdapter; 57a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa private GridLayoutManager mLayout; 5815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 59472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private TitleSearchHelper mSearchHelper; 60472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private Model mModel; 61472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 6274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa private int mLastFocusPosition = RecyclerView.NO_POSITION; 6374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 64472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public FocusManager(Context context, RecyclerView view, Model model) { 6515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa mView = view; 66472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mAdapter = (DocumentsAdapter) view.getAdapter(); 67a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa mLayout = (GridLayoutManager) view.getLayoutManager(); 68472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel = model; 69472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 70472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSearchHelper = new TitleSearchHelper(context); 7115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 7215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 7374956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 7415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 75472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Search helper gets first crack, for doing type-to-focus. 76472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mSearchHelper.handleKey(doc, keyCode, event)) { 77472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 78472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 79472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 8092db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa // Translate space/shift-space into PgDn/PgUp 8192db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa if (keyCode == KeyEvent.KEYCODE_SPACE) { 8292db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa if (event.isShiftPressed()) { 8392db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa keyCode = KeyEvent.KEYCODE_PAGE_UP; 8492db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } else { 8592db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa keyCode = KeyEvent.KEYCODE_PAGE_DOWN; 8692db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } 8792db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } 8892db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa 8915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 9015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find the target item and focus it. 9115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int endPos = findTargetPosition(doc.itemView, keyCode, event); 9215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 9315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (endPos != RecyclerView.NO_POSITION) { 9415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa focusItem(endPos); 9515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 9674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Swallow all navigation keystrokes. Otherwise they go to the app's global 9774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // key-handler, which will route them back to the DF and cause focus to be reset. 9874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return true; 9974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 10074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return false; 10174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 10274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 10374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa @Override 10474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa public void onFocusChange(View v, boolean hasFocus) { 10574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Remember focus events on items. 10674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa if (hasFocus && v.getParent() == mView) { 10774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa mLastFocusPosition = mView.getChildAdapterPosition(v); 10874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 10974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 11074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 11174956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 11274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa public void restoreLastFocus() { 113237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa if (mAdapter.getItemCount() == 0) { 114237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa // Nothing to focus. 115237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa return; 116237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa } 117237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa 11874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa if (mLastFocusPosition != RecyclerView.NO_POSITION) { 11974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // The system takes care of situations when a view is no longer on screen, etc, 12074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa focusItem(mLastFocusPosition); 12174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } else { 12274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Focus the first visible item 12374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa focusItem(mLayout.findFirstVisibleItemPosition()); 12415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 12515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 12615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 12774956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay @Override 1289504d764fc5b625661959ed6dcd190b9730d418dBen Kwa public int getFocusPosition() { 1299504d764fc5b625661959ed6dcd190b9730d418dBen Kwa return mLastFocusPosition; 1309504d764fc5b625661959ed6dcd190b9730d418dBen Kwa } 1319504d764fc5b625661959ed6dcd190b9730d418dBen Kwa 1329504d764fc5b625661959ed6dcd190b9730d418dBen Kwa /** 13315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Finds the destination position where the focus should land for a given navigation event. 13415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 13515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the event. 13615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode The key code for the event. 13715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 13815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION. 13915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 14015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findTargetPosition(View view, int keyCode, KeyEvent event) { 14115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 14215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_HOME: 14315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return 0; 14415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_END: 14515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return mAdapter.getItemCount() - 1; 14615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_UP: 14715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_DOWN: 14815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return findPagedTargetPosition(view, keyCode, event); 14915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 15015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 15115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find a navigation target based on the arrow key that the user pressed. 15215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int searchDir = -1; 15315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 15415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_UP: 15515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_UP; 15615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 15715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_DOWN: 15815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_DOWN; 15915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 160a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 161a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 162a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (inGridMode()) { 163a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa int currentPosition = mView.getChildAdapterPosition(view); 164a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Left and right arrow keys only work in grid mode. 165a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa switch (keyCode) { 166a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_LEFT: 167a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (currentPosition > 0) { 168a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop backward focus search at the first item, otherwise focus will wrap 169a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the last visible item. 170a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_BACKWARD; 171a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 172a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 173a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_RIGHT: 174a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (currentPosition < mAdapter.getItemCount() - 1) { 175a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop forward focus search at the last item, otherwise focus will wrap 176a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the first visible item. 177a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_FORWARD; 178a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 179a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 180a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 18115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 18215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 18315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (searchDir != -1) { 18467f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // Focus search behaves badly if the parent RecyclerView is focused. However, focusable 18567f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after 18667f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable 18767f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // off while performing the focus search. 18867f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // TODO: Revisit this when RV focus issues are resolved. 18967f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa mView.setFocusable(false); 19015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa View targetView = view.focusSearch(searchDir); 19167f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa mView.setFocusable(true); 19215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // TargetView can be null, for example, if the user pressed <down> at the bottom 19315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // of the list. 19415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (targetView != null) { 19515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Ignore navigation targets that aren't items in the RecyclerView. 19615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (targetView.getParent() == mView) { 19715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return mView.getChildAdapterPosition(targetView); 19815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 19915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 20215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return RecyclerView.NO_POSITION; 20315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 20515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 20615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Given a PgUp/PgDn event and the current view, find the position of the target view. 20715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * This returns: 20815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The position of the topmost (or bottom-most) visible item, if the current item is not 20915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * the top- or bottom-most visible item. 21015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The position of an item that is one page's worth of items up (or down) if the current 21115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * item is the top- or bottom-most visible item. 21215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The first (or last) item, if paging up (or down) would go past those limits. 21315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the key event. 21415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN. 21515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 21615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the target item. 21715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 21815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) { 21915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int first = mLayout.findFirstVisibleItemPosition(); 22015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int last = mLayout.findLastVisibleItemPosition(); 22115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int current = mView.getChildAdapterPosition(view); 22215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int pageSize = last - first + 1; 22315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 22415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { 22515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current > first) { 22615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the first item, target the first item. 22715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return first; 22815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 22915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the first item, target the item one page up. 23015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current - pageSize; 23115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < 0 ? 0 : target; 23215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 23315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 23415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 23515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) { 23615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current < last) { 23715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the last item, target the last item. 23815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return last; 23915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 24015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the last item, target the item one page down. 24115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current + pageSize; 24215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int max = mAdapter.getItemCount() - 1; 24315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < max ? target : max; 24415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 24515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 24615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 24715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa throw new IllegalArgumentException("Unsupported keyCode: " + keyCode); 24815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 24915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 25015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 25115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 25215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * necessary. 25315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 25415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param pos 25515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 25615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private void focusItem(final int pos) { 2576fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa focusItem(pos, null); 2586fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 2596fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 2606fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa /** 2616fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 2626fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * necessary. 2636fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 2646fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param pos 2656fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param callback A callback to call after the given item has been focused. 2666fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa */ 2676fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void focusItem(final int pos, @Nullable final FocusCallback callback) { 26815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the item is already in view, focus it; otherwise, scroll to it and focus it. 26915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); 27015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 2716fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 2726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa callback.onFocus(vh.itemView); 2736fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 27415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 27515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Set a one-time listener to request focus when the scroll has completed. 27615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa mView.addOnScrollListener( 27715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa new RecyclerView.OnScrollListener() { 27815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa @Override 27915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public void onScrollStateChanged(RecyclerView view, int newState) { 28015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (newState == RecyclerView.SCROLL_STATE_IDLE) { 28115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // When scrolling stops, find the item and focus it. 28215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa RecyclerView.ViewHolder vh = 28315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa view.findViewHolderForAdapterPosition(pos); 28415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 2856fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (vh.itemView.requestFocus() && callback != null) { 2866fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa callback.onFocus(vh.itemView); 2876fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 28815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 28915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // This might happen in weird corner cases, e.g. if the user is 29015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // scrolling while a delete operation is in progress. In that 29115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // case, just don't attempt to focus the missing item. 29215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa Log.w(TAG, "Unable to focus position " + pos + " after scroll"); 29315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 29415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa view.removeOnScrollListener(this); 29515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 29615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 29715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa }); 298472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mView.smoothScrollToPosition(pos); 29915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 30015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 301a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 302a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa /** 303a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa * @return Whether the layout manager is currently in a grid-configuration. 304a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa */ 305a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa private boolean inGridMode() { 306a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa return mLayout.getSpanCount() > 1; 307a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 308472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 3096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private interface FocusCallback { 3106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void onFocus(View view); 3116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 3126fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 313472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 314472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via 315472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build 316472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * up a string from individual key events, and perform searching based on that string. When an 317472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * item is found that matches the search term, that item will be focused. This class also 318472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * highlights instances of the search term found in the view. 319472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 320472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private class TitleSearchHelper { 3216fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa static private final int SEARCH_TIMEOUT = 500; // ms 3226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 3236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false); 3246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final Editable mSearchString = Editable.Factory.getInstance().newEditable(""); 3256fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final Highlighter mHighlighter = new Highlighter(); 3266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private final BackgroundColorSpan mSpan; 3276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 328472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private List<String> mIndex; 329472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private boolean mActive; 3306fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Timer mTimer; 3316fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private KeyEvent mLastEvent; 3326fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Handler mUiRunner; 333472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 334472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public TitleSearchHelper(Context context) { 335472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSpan = new BackgroundColorSpan(context.getColor(R.color.accent_dark)); 3366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Handler for running things on the main UI thread. Needed for updating the UI from a 3376fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // timer (see #activate, below). 3386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mUiRunner = new Handler(Looper.getMainLooper()); 339472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 340472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 341472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 342472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out 343472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * of individual key events, and then performs a search for the given string. 344472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * 345472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param doc The document holder receiving the key event. 346472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param keyCode 347472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param event 348472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @return Whether the event was handled. 349472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 350472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 351472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa switch (keyCode) { 352472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ESCAPE: 353472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ENTER: 354472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 355472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // These keys end any active searches. 3566fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 357472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 358472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 359472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle these key events if there is no active search. 360472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 361472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 362472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_SPACE: 363472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // This allows users to search for files with spaces in their names, but ignores 3646fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // spacebar events when a text search is not active. Ignoring the spacebar 3656fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // event is necessary because other handlers (see FocusManager#handleKey) also 3666fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // listen for and handle it. 367472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 368472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 369472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 370472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 371472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 372472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Navigation keys also end active searches. 373472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 3746fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 375472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle the keycode, so navigation still occurs. 376472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 377472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 378472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 379472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Build up the search string, and perform the search. 380472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event); 381472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 382472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Delete is processed by the text listener, but not "handled". Check separately for it. 3836fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (keyCode == KeyEvent.KEYCODE_DEL) { 3846fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa handled = true; 3856fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 3866fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 3876fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (handled) { 3886fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mLastEvent = event; 3896fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mSearchString.length() == 0) { 390472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't perform empty searches. 391472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 392472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 3936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa search(); 394472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 395472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 396472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return handled; 397472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 398472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 399472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 400472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Activates the search helper, which changes its key handling and updates the search index 401472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * and highlights if necessary. Call this each time the search term is updated. 402472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 4036fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void search() { 404472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 4056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // The model listener invalidates the search index when the model changes. 406472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel.addUpdateListener(mModelListener); 4076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 4086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Used to keep the current search alive until the timeout expires. If the user 4096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // presses another key within that time, that keystroke is added to the current 4106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // search. Otherwise, the current search ends, and subsequent keystrokes start a new 4116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // search. 4126fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer = new Timer(); 4136fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mActive = true; 414472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 415472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 416472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // If the search index was invalidated, rebuild it 417472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mIndex == null) { 418472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa buildIndex(); 419472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 420472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 4216fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Search for the current search term. 4226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Perform case-insensitive search. 4236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String searchString = mSearchString.toString().toLowerCase(); 4246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa for (int pos = 0; pos < mIndex.size(); pos++) { 4256fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa String title = mIndex.get(pos); 4266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (title != null && title.startsWith(searchString)) { 4276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa focusItem(pos, new FocusCallback() { 4286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa @Override 4296fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void onFocus(View view) { 4306fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mHighlighter.applyHighlight(view); 4316fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Using a timer repeat period of SEARCH_TIMEOUT/2 means the amount of 4326fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // time between the last keystroke and a search expiring is actually 4336fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // between 500 and 750 ms. A smaller timer period results in less 4346fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // variability but does more polling. 4356fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer.schedule(new TimeoutTask(), 0, SEARCH_TIMEOUT / 2); 4366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 4376fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa }); 4386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa break; 4396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 4406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 441472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 442472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 443472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 4446fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Ends the current search (see {@link #search()}. 445472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 4466fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void endSearch() { 447472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 448472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel.removeUpdateListener(mModelListener); 4496fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mTimer.cancel(); 450472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 451472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 4526fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mHighlighter.removeHighlight(); 453472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 454472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 455472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSearchString.clear(); 456472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mActive = false; 457472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 458472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 459472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 460472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Builds a search index for finding items by title. Queries the model and adapter, so both 461472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * must be set up before calling this method. 462472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 463472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void buildIndex() { 464472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa int itemCount = mAdapter.getItemCount(); 465472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa List<String> index = new ArrayList<>(itemCount); 466472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa for (int i = 0; i < itemCount; i++) { 467472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa String modelId = mAdapter.getModelId(i); 4685a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay Cursor cursor = mModel.getItem(modelId); 4695a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay if (modelId != null && cursor != null) { 4705a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay String title = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 4716fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // Perform case-insensitive search. 4726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa index.add(title.toLowerCase()); 473472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 474472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa index.add(""); 475472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 476472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 477472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = index; 478472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 479472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 480472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private Model.UpdateListener mModelListener = new Model.UpdateListener() { 481472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 482472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void onModelUpdate(Model model) { 483472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Invalidate the search index when the model updates. 484472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 485472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 486472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 487472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 488472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void onModelUpdateFailed(Exception e) { 489472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Invalidate the search index when the model updates. 490472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 491472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 492472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 493472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 4946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private class TimeoutTask extends TimerTask { 4956fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa @Override 4966fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void run() { 4976fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa long last = mLastEvent.getEventTime(); 4986fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa long now = SystemClock.uptimeMillis(); 4996fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if ((now - last) > SEARCH_TIMEOUT) { 5006fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa // endSearch must run on the main thread because it does UI work 5016fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mUiRunner.post(new Runnable() { 5026fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa @Override 5036fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa public void run() { 5046fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa endSearch(); 5056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 5066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa }); 507472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 508472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 5096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa }; 5106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa 5116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private class Highlighter { 5126fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private Spannable mCurrentHighlight; 513472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 514472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 5156fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Applies title highlights to the given view. The view must have a title field that is a 5166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * spannable text field. If this condition is not met, this function does nothing. 5176fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 5186fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param view 519472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 5206fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void applyHighlight(View view) { 5216fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa TextView titleView = (TextView) view.findViewById(android.R.id.title); 5226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (titleView == null) { 5236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa return; 524472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 525472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa CharSequence tmpText = titleView.getText(); 5276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (tmpText instanceof Spannable) { 5286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mCurrentHighlight != null) { 5296fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.removeSpan(mSpan); 5306fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 5316fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight = (Spannable) tmpText; 5326fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.setSpan( 5336fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mSpan, 0, mSearchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 5346fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa } 535472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 536472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5376fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa /** 5386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * Removes title highlights from the given view. The view must have a title field that is a 5396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * spannable text field. If this condition is not met, this function does nothing. 5406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * 5416fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa * @param view 5426fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa */ 5436fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa private void removeHighlight() { 5446fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa if (mCurrentHighlight != null) { 5456fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa mCurrentHighlight.removeSpan(mSpan); 546472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 547472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 548472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 549472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 55015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa} 551