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