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