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