FocusManager.java revision 237432ebb77eabd98d07f7fd09b808ac8d66753e
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
21472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.content.Context;
22472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.provider.DocumentsContract.Document;
23a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwaimport android.support.v7.widget.GridLayoutManager;
2415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.support.v7.widget.RecyclerView;
25472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Editable;
26472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Spannable;
27472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.KeyListener;
28472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener;
29472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener.Capitalize;
30472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.style.BackgroundColorSpan;
3115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.util.Log;
3215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.KeyEvent;
3315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.View;
34472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.widget.TextView;
3515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
3615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport com.android.documentsui.Events;
37472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport com.android.documentsui.R;
38472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
39472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.ArrayList;
40472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.List;
4115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
4215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa/**
4315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * A class that handles navigation and focus within the DirectoryFragment.
4415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */
4574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwaclass FocusManager implements View.OnFocusChangeListener {
4615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private static final String TAG = "FocusManager";
4715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
4815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private RecyclerView mView;
49472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    private DocumentsAdapter mAdapter;
50a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa    private GridLayoutManager mLayout;
5115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
52472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    private TitleSearchHelper mSearchHelper;
53472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    private Model mModel;
54472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
5574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    private int mLastFocusPosition = RecyclerView.NO_POSITION;
5674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa
57472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    public FocusManager(Context context, RecyclerView view, Model model) {
5815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        mView = view;
59472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        mAdapter = (DocumentsAdapter) view.getAdapter();
60a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa        mLayout = (GridLayoutManager) view.getLayoutManager();
61472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        mModel = model;
62472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
63472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        mSearchHelper = new TitleSearchHelper(context);
6415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
6515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
6615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    /**
6715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key
6815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * events.
6915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     *
7015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param doc The DocumentHolder receiving the key event.
7115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param keyCode
7215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param event
7315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @return Whether the event was handled.
7415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     */
7515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
76472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        // Search helper gets first crack, for doing type-to-focus.
77472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        if (mSearchHelper.handleKey(doc, keyCode, event)) {
78472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            return true;
79472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
80472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
8192db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa        // Translate space/shift-space into PgDn/PgUp
8292db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa        if (keyCode == KeyEvent.KEYCODE_SPACE) {
8392db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa            if (event.isShiftPressed()) {
8492db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa                keyCode = KeyEvent.KEYCODE_PAGE_UP;
8592db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa            } else {
8692db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa                keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
8792db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa            }
8892db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa        }
8992db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa
9015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (Events.isNavigationKeyCode(keyCode)) {
9115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            // Find the target item and focus it.
9215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            int endPos = findTargetPosition(doc.itemView, keyCode, event);
9315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
9415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            if (endPos != RecyclerView.NO_POSITION) {
9515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                focusItem(endPos);
9615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            }
9774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            // Swallow all navigation keystrokes. Otherwise they go to the app's global
9874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            // key-handler, which will route them back to the DF and cause focus to be reset.
9974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            return true;
10074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        }
10174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        return false;
10274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    }
10374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa
10474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    @Override
10574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    public void onFocusChange(View v, boolean hasFocus) {
10674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        // Remember focus events on items.
10774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        if (hasFocus && v.getParent() == mView) {
10874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            mLastFocusPosition = mView.getChildAdapterPosition(v);
10974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        }
11074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    }
11174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa
11274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    /**
11374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa     * Requests focus on the item that last had focus. Scrolls to that item if necessary.
11474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa     */
11574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    public void restoreLastFocus() {
116237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa        if (mAdapter.getItemCount() == 0) {
117237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa            // Nothing to focus.
118237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa            return;
119237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa        }
120237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa
12174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        if (mLastFocusPosition != RecyclerView.NO_POSITION) {
12274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            // The system takes care of situations when a view is no longer on screen, etc,
12374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            focusItem(mLastFocusPosition);
12474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        } else {
12574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            // Focus the first visible item
12674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            focusItem(mLayout.findFirstVisibleItemPosition());
12715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
12815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
12915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
13015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    /**
1319504d764fc5b625661959ed6dcd190b9730d418dBen Kwa     * @return The adapter position of the last focused item.
1329504d764fc5b625661959ed6dcd190b9730d418dBen Kwa     */
1339504d764fc5b625661959ed6dcd190b9730d418dBen Kwa    public int getFocusPosition() {
1349504d764fc5b625661959ed6dcd190b9730d418dBen Kwa        return mLastFocusPosition;
1359504d764fc5b625661959ed6dcd190b9730d418dBen Kwa    }
1369504d764fc5b625661959ed6dcd190b9730d418dBen Kwa
1379504d764fc5b625661959ed6dcd190b9730d418dBen Kwa    /**
13815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * Finds the destination position where the focus should land for a given navigation event.
13915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     *
14015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param view The view that received the event.
14115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param keyCode The key code for the event.
14215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param event
14315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION.
14415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     */
14515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private int findTargetPosition(View view, int keyCode, KeyEvent event) {
14615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        switch (keyCode) {
14715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_MOVE_HOME:
14815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return 0;
14915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_MOVE_END:
15015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return mAdapter.getItemCount() - 1;
15115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_PAGE_UP:
15215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_PAGE_DOWN:
15315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return findPagedTargetPosition(view, keyCode, event);
15415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
15515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
15615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        // Find a navigation target based on the arrow key that the user pressed.
15715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        int searchDir = -1;
15815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        switch (keyCode) {
15915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_DPAD_UP:
16015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                searchDir = View.FOCUS_UP;
16115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                break;
16215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_DPAD_DOWN:
16315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                searchDir = View.FOCUS_DOWN;
16415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                break;
165a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa        }
166a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa
167a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa        if (inGridMode()) {
168a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa            int currentPosition = mView.getChildAdapterPosition(view);
169a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa            // Left and right arrow keys only work in grid mode.
170a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa            switch (keyCode) {
171a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                case KeyEvent.KEYCODE_DPAD_LEFT:
172a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    if (currentPosition > 0) {
173a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        // Stop backward focus search at the first item, otherwise focus will wrap
174a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        // around to the last visible item.
175a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        searchDir = View.FOCUS_BACKWARD;
176a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    }
177a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    break;
178a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                case KeyEvent.KEYCODE_DPAD_RIGHT:
179a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    if (currentPosition < mAdapter.getItemCount() - 1) {
180a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        // Stop forward focus search at the last item, otherwise focus will wrap
181a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        // around to the first visible item.
182a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        searchDir = View.FOCUS_FORWARD;
183a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    }
184a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    break;
185a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa            }
18615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
18715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
18815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (searchDir != -1) {
18967f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // Focus search behaves badly if the parent RecyclerView is focused. However, focusable
19067f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after
19167f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable
19267f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // off while performing the focus search.
19367f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // TODO: Revisit this when RV focus issues are resolved.
19467f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            mView.setFocusable(false);
19515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            View targetView = view.focusSearch(searchDir);
19667f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            mView.setFocusable(true);
19715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            // TargetView can be null, for example, if the user pressed <down> at the bottom
19815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            // of the list.
19915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            if (targetView != null) {
20015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // Ignore navigation targets that aren't items in the RecyclerView.
20115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                if (targetView.getParent() == mView) {
20215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                    return mView.getChildAdapterPosition(targetView);
20315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                }
20415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            }
20515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
20615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
20715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        return RecyclerView.NO_POSITION;
20815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
20915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
21015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    /**
21115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * Given a PgUp/PgDn event and the current view, find the position of the target view.
21215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * This returns:
21315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * <li>The position of the topmost (or bottom-most) visible item, if the current item is not
21415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     *     the top- or bottom-most visible item.
21515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * <li>The position of an item that is one page's worth of items up (or down) if the current
21615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     *      item is the top- or bottom-most visible item.
21715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * <li>The first (or last) item, if paging up (or down) would go past those limits.
21815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param view The view that received the key event.
21915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN.
22015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param event
22115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @return The adapter position of the target item.
22215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     */
22315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) {
22415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        int first = mLayout.findFirstVisibleItemPosition();
22515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        int last = mLayout.findLastVisibleItemPosition();
22615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        int current = mView.getChildAdapterPosition(view);
22715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        int pageSize = last - first + 1;
22815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
22915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
23015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            if (current > first) {
23115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // If the current item isn't the first item, target the first item.
23215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return first;
23315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            } else {
23415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // If the current item is the first item, target the item one page up.
23515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                int target = current - pageSize;
23615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return target < 0 ? 0 : target;
23715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            }
23815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
23915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
24015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
24115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            if (current < last) {
24215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // If the current item isn't the last item, target the last item.
24315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return last;
24415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            } else {
24515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // If the current item is the last item, target the item one page down.
24615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                int target = current + pageSize;
24715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                int max = mAdapter.getItemCount() - 1;
24815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return target < max ? target : max;
24915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            }
25015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
25115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
25215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        throw new IllegalArgumentException("Unsupported keyCode: " + keyCode);
25315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
25415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
25515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    /**
25615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * Requests focus for the item in the given adapter position, scrolling the RecyclerView if
25715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * necessary.
25815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     *
25915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param pos
26015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     */
26115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private void focusItem(final int pos) {
26215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        // If the item is already in view, focus it; otherwise, scroll to it and focus it.
26315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
26415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (vh != null) {
26515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            vh.itemView.requestFocus();
26615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        } else {
26715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            // Set a one-time listener to request focus when the scroll has completed.
26815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            mView.addOnScrollListener(
26915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                    new RecyclerView.OnScrollListener() {
27015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                        @Override
27115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                        public void onScrollStateChanged(RecyclerView view, int newState) {
27215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
27315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                // When scrolling stops, find the item and focus it.
27415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                RecyclerView.ViewHolder vh =
27515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                        view.findViewHolderForAdapterPosition(pos);
27615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                if (vh != null) {
27715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    vh.itemView.requestFocus();
27815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                } else {
27915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    // This might happen in weird corner cases, e.g. if the user is
28015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    // scrolling while a delete operation is in progress. In that
28115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    // case, just don't attempt to focus the missing item.
28215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    Log.w(TAG, "Unable to focus position " + pos + " after scroll");
28315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                }
28415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                view.removeOnScrollListener(this);
28515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                            }
28615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                        }
28715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                    });
288472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mView.smoothScrollToPosition(pos);
28915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
29015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
291a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa
292a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa    /**
293a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa     * @return Whether the layout manager is currently in a grid-configuration.
294a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa     */
295a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa    private boolean inGridMode() {
296a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa        return mLayout.getSpanCount() > 1;
297a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa    }
298472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
299472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    /**
300472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via
301472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build
302472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * up a string from individual key events, and perform searching based on that string. When an
303472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * item is found that matches the search term, that item will be focused. This class also
304472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * highlights instances of the search term found in the view.
305472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     */
306472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    private class TitleSearchHelper {
307472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        final private KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false);
308472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        final private Editable mSearchString = Editable.Factory.getInstance().newEditable("");
309472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        final private Highlighter mHighlighter = new Highlighter();
310472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        final private BackgroundColorSpan mSpan;
311472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private List<String> mIndex;
312472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private boolean mActive;
313472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
314472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        public TitleSearchHelper(Context context) {
315472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mSpan = new BackgroundColorSpan(context.getColor(R.color.accent_dark));
316472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
317472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
318472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        /**
319472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out
320472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * of individual key events, and then performs a search for the given string.
321472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         *
322472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @param doc The document holder receiving the key event.
323472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @param keyCode
324472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @param event
325472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @return Whether the event was handled.
326472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         */
327472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
328472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            switch (keyCode) {
329472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                case KeyEvent.KEYCODE_ESCAPE:
330472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                case KeyEvent.KEYCODE_ENTER:
331472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    if (mActive) {
332472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        // These keys end any active searches.
333472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        deactivate();
334472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        return true;
335472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    } else {
336472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        // Don't handle these key events if there is no active search.
337472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        return false;
338472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    }
339472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                case KeyEvent.KEYCODE_SPACE:
340472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    // This allows users to search for files with spaces in their names, but ignores
341472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    // spacebar events when a text search is not active.
342472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    if (!mActive) {
343472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        return false;
344472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    }
345472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
346472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
347472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // Navigation keys also end active searches.
348472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (Events.isNavigationKeyCode(keyCode)) {
349472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                deactivate();
350472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Don't handle the keycode, so navigation still occurs.
351472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                return false;
352472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
353472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
354472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // Build up the search string, and perform the search.
355472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event);
356472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
357472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // Delete is processed by the text listener, but not "handled". Check separately for it.
358472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (handled || keyCode == KeyEvent.KEYCODE_DEL) {
359472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                String searchString = mSearchString.toString();
360472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                if (searchString.length() == 0) {
361472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    // Don't perform empty searches.
362472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    return false;
363472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
364472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                activate();
365472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                for (int pos = 0; pos < mIndex.size(); pos++) {
366472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    String title = mIndex.get(pos);
367472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    if (title != null && title.startsWith(searchString)) {
368472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        focusItem(pos);
369472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        break;
370472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    }
371472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
372472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
373472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
374472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            return handled;
375472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
376472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
377472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        /**
378472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * Activates the search helper, which changes its key handling and updates the search index
379472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * and highlights if necessary. Call this each time the search term is updated.
380472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         */
381472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private void activate() {
382472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (!mActive) {
383472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Install listeners.
384472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                mModel.addUpdateListener(mModelListener);
385472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
386472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
387472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // If the search index was invalidated, rebuild it
388472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (mIndex == null) {
389472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                buildIndex();
390472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
391472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
392472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // TODO: Uncomment this to enable search term highlighting in the UI.
393472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa//            mHighlighter.activate();
394472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
395472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mActive = true;
396472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
397472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
398472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        /**
399472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * Deactivates the search helper (see {@link #activate()}). Call this when a search ends.
400472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         */
401472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private void deactivate() {
402472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (mActive) {
403472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Remove listeners.
404472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                mModel.removeUpdateListener(mModelListener);
405472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
406472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
407472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // TODO: Uncomment this when search-term highlighting is enabled in the UI.
408472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa//            mHighlighter.deactivate();
409472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
410472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mIndex = null;
411472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mSearchString.clear();
412472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mActive = false;
413472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
414472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
415472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        /**
416472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * Applies title highlights to the given view. The view must have a title field that is a
417472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * spannable text field.  If this condition is not met, this function does nothing.
418472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         *
419472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @param view
420472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         */
421472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private void applyHighlight(View view) {
422472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            TextView titleView = (TextView) view.findViewById(android.R.id.title);
423472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (titleView == null) {
424472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                return;
425472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
426472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
427472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            String searchString = mSearchString.toString();
428472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            CharSequence tmpText = titleView.getText();
429472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (tmpText instanceof Spannable) {
430472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                Spannable title = (Spannable) tmpText;
431472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                String titleString = title.toString();
432472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                if (titleString.startsWith(searchString)) {
433472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    title.setSpan(mSpan, 0, searchString.length(),
434472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
435472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                } else {
436472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    title.removeSpan(mSpan);
437472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
438472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
439472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
440472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
441472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        /**
442472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * Removes title highlights from the given view. The view must have a title field that is a
443472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * spannable text field.  If this condition is not met, this function does nothing.
444472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         *
445472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @param view
446472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         */
447472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private void removeHighlight(View view) {
448472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            TextView titleView = (TextView) view.findViewById(android.R.id.title);
449472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (titleView == null) {
450472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                return;
451472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
452472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
453472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            CharSequence tmpText = titleView.getText();
454472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (tmpText instanceof Spannable) {
455472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                ((Spannable) tmpText).removeSpan(mSpan);
456472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
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);
468472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                if (modelId != null) {
469472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    index.add(
470472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                            getCursorString(mModel.getItem(modelId), Document.COLUMN_DISPLAY_NAME));
471472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                } else {
472472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    index.add("");
473472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
474472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
475472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mIndex = index;
476472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
477472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
478472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private Model.UpdateListener mModelListener = new Model.UpdateListener() {
479472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            @Override
480472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            public void onModelUpdate(Model model) {
481472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Invalidate the search index when the model updates.
482472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                mIndex = null;
483472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
484472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
485472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            @Override
486472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            public void onModelUpdateFailed(Exception e) {
487472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Invalidate the search index when the model updates.
488472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                mIndex = null;
489472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
490472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        };
491472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
492472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private class Highlighter implements RecyclerView.OnChildAttachStateChangeListener {
493472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            /**
494472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa             * Starts highlighting instances of the current search term in the UI.
495472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa             */
496472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            public void activate() {
497472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Update highlights on all views
498472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                int itemCount = mView.getChildCount();
499472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                for (int i = 0; i < itemCount; i++) {
500472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    applyHighlight(mView.getChildAt(i));
501472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
502472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Keep highlights up-to-date as items come in and out of view.
503472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                mView.addOnChildAttachStateChangeListener(this);
504472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
505472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
506472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            /**
507472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa             * Stops highlighting instances of the current search term in the UI.
508472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa             */
509472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            public void deactivate() {
510472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Remove highlights on all views
511472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                int itemCount = mView.getChildCount();
512472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                for (int i = 0; i < itemCount; i++) {
513472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    removeHighlight(mView.getChildAt(i));
514472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
515472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Stop updating highlights.
516472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                mView.removeOnChildAttachStateChangeListener(this);
517472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
518472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
519472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            @Override
520472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            public void onChildViewAttachedToWindow(View view) {
521472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                applyHighlight(view);
522472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
523472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
524472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            @Override
525472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            public void onChildViewDetachedFromWindow(View view) {
526472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                TextView titleView = (TextView) view.findViewById(android.R.id.title);
527472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                if (titleView != null) {
528472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    removeHighlight(titleView);
529472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
530472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
531472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        };
532472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    }
53315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa}
534