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
1717b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKaypackage com.android.documentsui;
1815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
19d080506e3aa8547605cd4783eb660775d7d2b8eeSteve McKayimport static com.android.documentsui.base.DocumentInfo.getCursorString;
2075b7b9039cf0efcb188e916c6f510328bfe099a8Ben Linimport static com.android.documentsui.base.Shared.DEBUG;
2198f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKayimport static com.android.internal.util.Preconditions.checkNotNull;
22472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
2381afd7f587176e7d63f00d533b1258dfec84bf5cBen Linimport android.annotation.ColorRes;
246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.annotation.Nullable;
255a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKayimport android.database.Cursor;
266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Handler;
276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.Looper;
286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport android.os.SystemClock;
29472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.provider.DocumentsContract.Document;
30a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwaimport android.support.v7.widget.GridLayoutManager;
3115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.support.v7.widget.RecyclerView;
32472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Editable;
33472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Spannable;
34472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.KeyListener;
35472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener;
36472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener.Capitalize;
37472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.style.BackgroundColorSpan;
3815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.util.Log;
3915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.KeyEvent;
4015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.View;
41472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.widget.TextView;
4215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
43990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKayimport com.android.documentsui.base.EventListener;
44d9caa6ab53aa784acaf241c0ded3c4ae2d342bf8Steve McKayimport com.android.documentsui.base.Events;
4598f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKayimport com.android.documentsui.base.Features;
46047182631669608af946480c2545a10acb2ef1bfSteve McKayimport com.android.documentsui.base.Procedure;
4717b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.DocumentHolder;
4817b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.DocumentsAdapter;
4917b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKayimport com.android.documentsui.dirlist.FocusHandler;
50e967033315ed64bca8c89d601d187fd12754f1fbGarfield Tanimport com.android.documentsui.Model.Update;
5175b7b9039cf0efcb188e916c6f510328bfe099a8Ben Linimport com.android.documentsui.selection.SelectionManager;
52472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
53472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.ArrayList;
54472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.List;
556fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.Timer;
566fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwaimport java.util.TimerTask;
5715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
5881afd7f587176e7d63f00d533b1258dfec84bf5cBen Linpublic final class FocusManager implements FocusHandler {
5915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private static final String TAG = "FocusManager";
6015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
615b0a2c187a9e446b683687817d22cbe443585223Steve McKay    private final ContentScope mScope = new ContentScope();
62047182631669608af946480c2545a10acb2ef1bfSteve McKay
6398f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKay    private final Features mFeatures;
6475b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin    private final SelectionManager mSelectionMgr;
65047182631669608af946480c2545a10acb2ef1bfSteve McKay    private final DrawerController mDrawer;
66047182631669608af946480c2545a10acb2ef1bfSteve McKay    private final Procedure mRootsFocuser;
67047182631669608af946480c2545a10acb2ef1bfSteve McKay    private final TitleSearchHelper mSearchHelper;
68047182631669608af946480c2545a10acb2ef1bfSteve McKay
69047182631669608af946480c2545a10acb2ef1bfSteve McKay    private boolean mNavDrawerHasFocus;
70047182631669608af946480c2545a10acb2ef1bfSteve McKay
71047182631669608af946480c2545a10acb2ef1bfSteve McKay    public FocusManager(
7298f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKay            Features features,
73047182631669608af946480c2545a10acb2ef1bfSteve McKay            SelectionManager selectionMgr,
74047182631669608af946480c2545a10acb2ef1bfSteve McKay            DrawerController drawer,
75047182631669608af946480c2545a10acb2ef1bfSteve McKay            Procedure rootsFocuser,
76047182631669608af946480c2545a10acb2ef1bfSteve McKay            @ColorRes int color) {
7774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa
7898f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKay        mFeatures = checkNotNull(features);
7975b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        mSelectionMgr = selectionMgr;
80047182631669608af946480c2545a10acb2ef1bfSteve McKay        mDrawer = drawer;
81047182631669608af946480c2545a10acb2ef1bfSteve McKay        mRootsFocuser = rootsFocuser;
82047182631669608af946480c2545a10acb2ef1bfSteve McKay
8381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin        mSearchHelper = new TitleSearchHelper(color);
8415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
8515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
8674956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay    @Override
87047182631669608af946480c2545a10acb2ef1bfSteve McKay    public boolean advanceFocusArea() {
88340ab17f468789bb507daeae116cf7940ba84b03Ben Lin        // This should only be called in pre-O devices.
89340ab17f468789bb507daeae116cf7940ba84b03Ben Lin        // O has built-in keyboard navigation support.
9098f8c5f502e049a6b85439d773949cdbaa0f78aeSteve McKay        assert(!mFeatures.isSystemKeyboardNavigationEnabled());
9192ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay        boolean focusChanged = false;
92047182631669608af946480c2545a10acb2ef1bfSteve McKay        if (mNavDrawerHasFocus) {
93047182631669608af946480c2545a10acb2ef1bfSteve McKay            mDrawer.setOpen(false);
94b62d4e5804d807703697ad7eeb85131a35ce4ab4Ben Lin            focusChanged = focusDirectoryList();
95047182631669608af946480c2545a10acb2ef1bfSteve McKay        } else {
96047182631669608af946480c2545a10acb2ef1bfSteve McKay            mDrawer.setOpen(true);
9792ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay            focusChanged = mRootsFocuser.run();
98047182631669608af946480c2545a10acb2ef1bfSteve McKay        }
99047182631669608af946480c2545a10acb2ef1bfSteve McKay
10092ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay        if (focusChanged) {
101047182631669608af946480c2545a10acb2ef1bfSteve McKay            mNavDrawerHasFocus = !mNavDrawerHasFocus;
102047182631669608af946480c2545a10acb2ef1bfSteve McKay            return true;
103047182631669608af946480c2545a10acb2ef1bfSteve McKay        }
104047182631669608af946480c2545a10acb2ef1bfSteve McKay
105047182631669608af946480c2545a10acb2ef1bfSteve McKay        return false;
106047182631669608af946480c2545a10acb2ef1bfSteve McKay    }
107047182631669608af946480c2545a10acb2ef1bfSteve McKay
108047182631669608af946480c2545a10acb2ef1bfSteve McKay    @Override
10915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
110472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        // Search helper gets first crack, for doing type-to-focus.
111472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        if (mSearchHelper.handleKey(doc, keyCode, event)) {
112472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            return true;
113472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
114472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
11515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (Events.isNavigationKeyCode(keyCode)) {
11615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            // Find the target item and focus it.
11715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            int endPos = findTargetPosition(doc.itemView, keyCode, event);
11815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
11915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            if (endPos != RecyclerView.NO_POSITION) {
12015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                focusItem(endPos);
12115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            }
12274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            // Swallow all navigation keystrokes. Otherwise they go to the app's global
12374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            // key-handler, which will route them back to the DF and cause focus to be reset.
12474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa            return true;
12574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        }
12674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        return false;
12774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    }
12874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa
12974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    @Override
13074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    public void onFocusChange(View v, boolean hasFocus) {
13174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        // Remember focus events on items.
1325b0a2c187a9e446b683687817d22cbe443585223Steve McKay        if (hasFocus && v.getParent() == mScope.view) {
1335b0a2c187a9e446b683687817d22cbe443585223Steve McKay            mScope.lastFocusPosition = mScope.view.getChildAdapterPosition(v);
13474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa        }
13574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa    }
13674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa
13774956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay    @Override
138047182631669608af946480c2545a10acb2ef1bfSteve McKay    public boolean focusDirectoryList() {
1395b0a2c187a9e446b683687817d22cbe443585223Steve McKay        if (mScope.adapter.getItemCount() == 0) {
14092ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay            if (DEBUG) Log.v(TAG, "Nothing to focus.");
14175b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin            return false;
142237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa        }
143237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa
14475b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        // If there's a selection going on, we don't want to grant user the ability to focus
145047182631669608af946480c2545a10acb2ef1bfSteve McKay        // on any individfocusSomethingual item to prevent ambiguity in operations (Cut selection
146047182631669608af946480c2545a10acb2ef1bfSteve McKay        // vs. Cut focused
14775b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        // item)
14875b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        if (mSelectionMgr.hasSelection()) {
14992ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay            if (DEBUG) Log.v(TAG, "Existing selection found. No focus will be done.");
15075b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin            return false;
15115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
15275b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin
15375b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        final int focusPos = (mScope.lastFocusPosition != RecyclerView.NO_POSITION)
154047182631669608af946480c2545a10acb2ef1bfSteve McKay                ? mScope.lastFocusPosition
155047182631669608af946480c2545a10acb2ef1bfSteve McKay                : mScope.layout.findFirstVisibleItemPosition();
15675b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        focusItem(focusPos);
15775b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        return true;
15815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
15915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
16081afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin    /*
16181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin     * Attempts to reset focus on the item corresponding to {@code mPendingFocusId} if it exists and
16281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin     * has a valid position in the adapter. It then automatically resets {@code mPendingFocusId}.
16381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin     */
16481afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin    @Override
16581afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin    public void onLayoutCompleted() {
1665b0a2c187a9e446b683687817d22cbe443585223Steve McKay        if (mScope.pendingFocusId == null) {
16781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin            return;
16881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin        }
16981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin
1705b0a2c187a9e446b683687817d22cbe443585223Steve McKay        int pos = mScope.adapter.getModelIds().indexOf(mScope.pendingFocusId);
17181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin        if (pos != -1) {
17281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin            focusItem(pos);
17381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin        }
1745b0a2c187a9e446b683687817d22cbe443585223Steve McKay        mScope.pendingFocusId = null;
17581afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin    }
17681afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin
17781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin    /*
17881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin     * Attempts to put focus on the document associated with the given modelId. If item does not
179047182631669608af946480c2545a10acb2ef1bfSteve McKay     * exist yet in the layout, this sets a pending modelId to be used when {@code
180047182631669608af946480c2545a10acb2ef1bfSteve McKay     * #applyPendingFocus()} is called next time.
18181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin     */
18281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin    @Override
18317b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay    public void focusDocument(String modelId) {
1845b0a2c187a9e446b683687817d22cbe443585223Steve McKay        int pos = mScope.adapter.getModelIds().indexOf(modelId);
1855b0a2c187a9e446b683687817d22cbe443585223Steve McKay        if (pos != -1 && mScope.view.findViewHolderForAdapterPosition(pos) != null) {
18681afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin            focusItem(pos);
18781afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin        } else {
1885b0a2c187a9e446b683687817d22cbe443585223Steve McKay            mScope.pendingFocusId = modelId;
18981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin        }
19081afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin    }
19181afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin
19274956af50b13b5ffde252a13547c960ba3e9c5b4Steve McKay    @Override
1939504d764fc5b625661959ed6dcd190b9730d418dBen Kwa    public int getFocusPosition() {
1945b0a2c187a9e446b683687817d22cbe443585223Steve McKay        return mScope.lastFocusPosition;
1959504d764fc5b625661959ed6dcd190b9730d418dBen Kwa    }
1969504d764fc5b625661959ed6dcd190b9730d418dBen Kwa
197d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin    @Override
19875b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin    public boolean hasFocusedItem() {
19975b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin        return mScope.lastFocusPosition != RecyclerView.NO_POSITION;
20075b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin    }
20175b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin
20275b7b9039cf0efcb188e916c6f510328bfe099a8Ben Lin    @Override
203d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin    public @Nullable String getFocusModelId() {
204d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin        if (mScope.lastFocusPosition != RecyclerView.NO_POSITION) {
205d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin            DocumentHolder holder = (DocumentHolder) mScope.view
206d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin                    .findViewHolderForAdapterPosition(mScope.lastFocusPosition);
207d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin            return holder.getModelId();
208d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin        }
209d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin        return null;
210d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin    }
211d947f0192142c7db40d7dfaa8d0c6caaa1cf7c36Ben Lin
2129504d764fc5b625661959ed6dcd190b9730d418dBen Kwa    /**
21315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * Finds the destination position where the focus should land for a given navigation event.
21415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     *
21515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param view The view that received the event.
21615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param keyCode The key code for the event.
21715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param event
21815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION.
21915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     */
22015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private int findTargetPosition(View view, int keyCode, KeyEvent event) {
22115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        switch (keyCode) {
22215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_MOVE_HOME:
22315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return 0;
22415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_MOVE_END:
2255b0a2c187a9e446b683687817d22cbe443585223Steve McKay                return mScope.adapter.getItemCount() - 1;
22615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_PAGE_UP:
22715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_PAGE_DOWN:
22815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return findPagedTargetPosition(view, keyCode, event);
22915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
23015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
23115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        // Find a navigation target based on the arrow key that the user pressed.
23215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        int searchDir = -1;
23315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        switch (keyCode) {
23415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_DPAD_UP:
23515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                searchDir = View.FOCUS_UP;
23615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                break;
23715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            case KeyEvent.KEYCODE_DPAD_DOWN:
23815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                searchDir = View.FOCUS_DOWN;
23915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                break;
240a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa        }
241a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa
242a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa        if (inGridMode()) {
2435b0a2c187a9e446b683687817d22cbe443585223Steve McKay            int currentPosition = mScope.view.getChildAdapterPosition(view);
244a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa            // Left and right arrow keys only work in grid mode.
245a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa            switch (keyCode) {
246a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                case KeyEvent.KEYCODE_DPAD_LEFT:
247a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    if (currentPosition > 0) {
248a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        // Stop backward focus search at the first item, otherwise focus will wrap
249a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        // around to the last visible item.
250a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        searchDir = View.FOCUS_BACKWARD;
251a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    }
252a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    break;
253a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                case KeyEvent.KEYCODE_DPAD_RIGHT:
2545b0a2c187a9e446b683687817d22cbe443585223Steve McKay                    if (currentPosition < mScope.adapter.getItemCount() - 1) {
255a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        // Stop forward focus search at the last item, otherwise focus will wrap
256a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        // around to the first visible item.
257a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                        searchDir = View.FOCUS_FORWARD;
258a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    }
259a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa                    break;
260a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa            }
26115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
26215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
26315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (searchDir != -1) {
26467f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // Focus search behaves badly if the parent RecyclerView is focused. However, focusable
26567f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after
26667f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable
26767f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // off while performing the focus search.
26867f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa            // TODO: Revisit this when RV focus issues are resolved.
2695b0a2c187a9e446b683687817d22cbe443585223Steve McKay            mScope.view.setFocusable(false);
27015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            View targetView = view.focusSearch(searchDir);
2715b0a2c187a9e446b683687817d22cbe443585223Steve McKay            mScope.view.setFocusable(true);
27215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            // TargetView can be null, for example, if the user pressed <down> at the bottom
27315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            // of the list.
27415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            if (targetView != null) {
27515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // Ignore navigation targets that aren't items in the RecyclerView.
2765b0a2c187a9e446b683687817d22cbe443585223Steve McKay                if (targetView.getParent() == mScope.view) {
2775b0a2c187a9e446b683687817d22cbe443585223Steve McKay                    return mScope.view.getChildAdapterPosition(targetView);
27815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                }
27915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            }
28015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
28115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
28215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        return RecyclerView.NO_POSITION;
28315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
28415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
28515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    /**
286047182631669608af946480c2545a10acb2ef1bfSteve McKay     * Given a PgUp/PgDn event and the current view, find the position of the target view. This
287047182631669608af946480c2545a10acb2ef1bfSteve McKay     * returns:
288047182631669608af946480c2545a10acb2ef1bfSteve McKay     * <li>The position of the topmost (or bottom-most) visible item, if the current item is not the
289047182631669608af946480c2545a10acb2ef1bfSteve McKay     * top- or bottom-most visible item.
29015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * <li>The position of an item that is one page's worth of items up (or down) if the current
291047182631669608af946480c2545a10acb2ef1bfSteve McKay     * item is the top- or bottom-most visible item.
29215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * <li>The first (or last) item, if paging up (or down) would go past those limits.
293047182631669608af946480c2545a10acb2ef1bfSteve McKay     *
29415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param view The view that received the key event.
29515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN.
29615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param event
29715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @return The adapter position of the target item.
29815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     */
29915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) {
3005b0a2c187a9e446b683687817d22cbe443585223Steve McKay        int first = mScope.layout.findFirstVisibleItemPosition();
3015b0a2c187a9e446b683687817d22cbe443585223Steve McKay        int last = mScope.layout.findLastVisibleItemPosition();
3025b0a2c187a9e446b683687817d22cbe443585223Steve McKay        int current = mScope.view.getChildAdapterPosition(view);
30315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        int pageSize = last - first + 1;
30415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
30515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
30615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            if (current > first) {
30715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // If the current item isn't the first item, target the first item.
30815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return first;
30915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            } else {
31015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // If the current item is the first item, target the item one page up.
31115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                int target = current - pageSize;
31215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return target < 0 ? 0 : target;
31315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            }
31415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
31515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
31615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
31715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            if (current < last) {
31815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // If the current item isn't the last item, target the last item.
31915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return last;
32015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            } else {
32115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                // If the current item is the last item, target the item one page down.
32215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                int target = current + pageSize;
3235b0a2c187a9e446b683687817d22cbe443585223Steve McKay                int max = mScope.adapter.getItemCount() - 1;
32415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                return target < max ? target : max;
32515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            }
32615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
32715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
32815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        throw new IllegalArgumentException("Unsupported keyCode: " + keyCode);
32915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
33015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa
33115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    /**
33215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * Requests focus for the item in the given adapter position, scrolling the RecyclerView if
33315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * necessary.
33415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     *
33515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     * @param pos
33615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa     */
33715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    private void focusItem(final int pos) {
3386fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        focusItem(pos, null);
3396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa    }
3406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa
3416fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa    /**
3426fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa     * Requests focus for the item in the given adapter position, scrolling the RecyclerView if
3436fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa     * necessary.
3446fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa     *
3456fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa     * @param pos
3466fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa     * @param callback A callback to call after the given item has been focused.
3476fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa     */
34892ae43d5d22331aad83e1a4302a7e1975f66354eSteve McKay    private void focusItem(final int pos, @Nullable final FocusCallback callback) {
3495b0a2c187a9e446b683687817d22cbe443585223Steve McKay        if (mScope.pendingFocusId != null) {
3505b0a2c187a9e446b683687817d22cbe443585223Steve McKay            Log.v(TAG, "clearing pending focus id: " + mScope.pendingFocusId);
3515b0a2c187a9e446b683687817d22cbe443585223Steve McKay            mScope.pendingFocusId = null;
35281afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin        }
35381afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin
35415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        // If the item is already in view, focus it; otherwise, scroll to it and focus it.
3555b0a2c187a9e446b683687817d22cbe443585223Steve McKay        RecyclerView.ViewHolder vh = mScope.view.findViewHolderForAdapterPosition(pos);
35615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        if (vh != null) {
3576fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            if (vh.itemView.requestFocus() && callback != null) {
3586fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                callback.onFocus(vh.itemView);
3596fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            }
36015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        } else {
36115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa            // Set a one-time listener to request focus when the scroll has completed.
3625b0a2c187a9e446b683687817d22cbe443585223Steve McKay            mScope.view.addOnScrollListener(
36315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                    new RecyclerView.OnScrollListener() {
36415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                        @Override
36515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                        public void onScrollStateChanged(RecyclerView view, int newState) {
36615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
36715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                // When scrolling stops, find the item and focus it.
368047182631669608af946480c2545a10acb2ef1bfSteve McKay                                RecyclerView.ViewHolder vh = view
369047182631669608af946480c2545a10acb2ef1bfSteve McKay                                        .findViewHolderForAdapterPosition(pos);
37015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                if (vh != null) {
3716fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                                    if (vh.itemView.requestFocus() && callback != null) {
3726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                                        callback.onFocus(vh.itemView);
3736fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                                    }
37415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                } else {
37515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    // This might happen in weird corner cases, e.g. if the user is
37615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    // scrolling while a delete operation is in progress. In that
37715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    // case, just don't attempt to focus the missing item.
37815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                    Log.w(TAG, "Unable to focus position " + pos + " after scroll");
37915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                }
38015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                                view.removeOnScrollListener(this);
38115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                            }
38215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                        }
38315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa                    });
3845b0a2c187a9e446b683687817d22cbe443585223Steve McKay            mScope.view.smoothScrollToPosition(pos);
38515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa        }
38615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa    }
387a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa
388047182631669608af946480c2545a10acb2ef1bfSteve McKay    /** @return Whether the layout manager is currently in a grid-configuration. */
389a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa    private boolean inGridMode() {
3905b0a2c187a9e446b683687817d22cbe443585223Steve McKay        return mScope.layout.getSpanCount() > 1;
391a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa    }
392472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
3936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa    private interface FocusCallback {
3946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        public void onFocus(View view);
3956fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa    }
3966fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa
397472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    /**
398472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via
399472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build
400472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * up a string from individual key events, and perform searching based on that string. When an
401472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * item is found that matches the search term, that item will be focused. This class also
402472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     * highlights instances of the search term found in the view.
403472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa     */
404472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    private class TitleSearchHelper {
405047182631669608af946480c2545a10acb2ef1bfSteve McKay        private static final int SEARCH_TIMEOUT = 500; // ms
4066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa
4076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private final KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false);
4086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private final Editable mSearchString = Editable.Factory.getInstance().newEditable("");
4096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private final Highlighter mHighlighter = new Highlighter();
4106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private final BackgroundColorSpan mSpan;
4116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa
412472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private List<String> mIndex;
413472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private boolean mActive;
4146fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private Timer mTimer;
4156fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private KeyEvent mLastEvent;
4166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private Handler mUiRunner;
417472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
41881afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin        public TitleSearchHelper(@ColorRes int color) {
41981afd7f587176e7d63f00d533b1258dfec84bf5cBen Lin            mSpan = new BackgroundColorSpan(color);
4206fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            // Handler for running things on the main UI thread. Needed for updating the UI from a
4216fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            // timer (see #activate, below).
4226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            mUiRunner = new Handler(Looper.getMainLooper());
423472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
424472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
425472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        /**
426472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out
427472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * of individual key events, and then performs a search for the given string.
428472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         *
429472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @param doc The document holder receiving the key event.
430472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @param keyCode
431472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @param event
432472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * @return Whether the event was handled.
433472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         */
434472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
435472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            switch (keyCode) {
436472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                case KeyEvent.KEYCODE_ESCAPE:
437472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                case KeyEvent.KEYCODE_ENTER:
438472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    if (mActive) {
439472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        // These keys end any active searches.
4406fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                        endSearch();
441472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        return true;
442472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    } else {
443472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        // Don't handle these key events if there is no active search.
444472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        return false;
445472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    }
446472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                case KeyEvent.KEYCODE_SPACE:
447472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    // This allows users to search for files with spaces in their names, but ignores
4486fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    // spacebar events when a text search is not active. Ignoring the spacebar
4496fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    // event is necessary because other handlers (see FocusManager#handleKey) also
4506fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    // listen for and handle it.
451472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    if (!mActive) {
452472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                        return false;
453472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    }
454472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
455472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
456472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // Navigation keys also end active searches.
457472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (Events.isNavigationKeyCode(keyCode)) {
4586fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                endSearch();
459472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Don't handle the keycode, so navigation still occurs.
460472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                return false;
461472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
462472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
463472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // Build up the search string, and perform the search.
464472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event);
465472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
466472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // Delete is processed by the text listener, but not "handled". Check separately for it.
4676fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            if (keyCode == KeyEvent.KEYCODE_DEL) {
4686fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                handled = true;
4696fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            }
4706fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa
4716fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            if (handled) {
4726fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                mLastEvent = event;
4736fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                if (mSearchString.length() == 0) {
474472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    // Don't perform empty searches.
475472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    return false;
476472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
4776fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                search();
478472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
479472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
480472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            return handled;
481472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
482472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
483472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        /**
484472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * Activates the search helper, which changes its key handling and updates the search index
485472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * and highlights if necessary. Call this each time the search term is updated.
486472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         */
4876fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private void search() {
488472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (!mActive) {
4896fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                // The model listener invalidates the search index when the model changes.
4905b0a2c187a9e446b683687817d22cbe443585223Steve McKay                mScope.model.addUpdateListener(mModelListener);
4916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa
4926fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                // Used to keep the current search alive until the timeout expires. If the user
4936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                // presses another key within that time, that keystroke is added to the current
4946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                // search. Otherwise, the current search ends, and subsequent keystrokes start a new
4956fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                // search.
4966fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                mTimer = new Timer();
4976fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                mActive = true;
498472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
499472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
500472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            // If the search index was invalidated, rebuild it
501472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (mIndex == null) {
502472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                buildIndex();
503472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
504472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
5056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            // Search for the current search term.
5066fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            // Perform case-insensitive search.
5076fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            String searchString = mSearchString.toString().toLowerCase();
5086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            for (int pos = 0; pos < mIndex.size(); pos++) {
5096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                String title = mIndex.get(pos);
5106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                if (title != null && title.startsWith(searchString)) {
511047182631669608af946480c2545a10acb2ef1bfSteve McKay                    focusItem(
512047182631669608af946480c2545a10acb2ef1bfSteve McKay                            pos,
513047182631669608af946480c2545a10acb2ef1bfSteve McKay                            new FocusCallback() {
514047182631669608af946480c2545a10acb2ef1bfSteve McKay                                @Override
515047182631669608af946480c2545a10acb2ef1bfSteve McKay                                public void onFocus(View view) {
516047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    mHighlighter.applyHighlight(view);
517047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    // Using a timer repeat period of SEARCH_TIMEOUT/2 means the
518047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    // amount of
519047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    // time between the last keystroke and a search expiring is
520047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    // actually
521047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    // between 500 and 750 ms. A smaller timer period results in
522047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    // less
523047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    // variability but does more polling.
524047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    mTimer.schedule(new TimeoutTask(), 0, SEARCH_TIMEOUT / 2);
525047182631669608af946480c2545a10acb2ef1bfSteve McKay                                }
526047182631669608af946480c2545a10acb2ef1bfSteve McKay                            });
5276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    break;
5286fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                }
5296fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            }
530472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
531472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
532047182631669608af946480c2545a10acb2ef1bfSteve McKay        /** Ends the current search (see {@link #search()}. */
5336fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private void endSearch() {
534472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            if (mActive) {
5355b0a2c187a9e446b683687817d22cbe443585223Steve McKay                mScope.model.removeUpdateListener(mModelListener);
5366fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                mTimer.cancel();
537472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
538472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
5396fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            mHighlighter.removeHighlight();
540472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
541472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mIndex = null;
542472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mSearchString.clear();
543472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mActive = false;
544472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
545472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
546472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        /**
547472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * Builds a search index for finding items by title. Queries the model and adapter, so both
548472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         * must be set up before calling this method.
549472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa         */
550472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        private void buildIndex() {
5515b0a2c187a9e446b683687817d22cbe443585223Steve McKay            int itemCount = mScope.adapter.getItemCount();
552472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            List<String> index = new ArrayList<>(itemCount);
553472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            for (int i = 0; i < itemCount; i++) {
5545b0a2c187a9e446b683687817d22cbe443585223Steve McKay                String modelId = mScope.adapter.getModelId(i);
5555b0a2c187a9e446b683687817d22cbe443585223Steve McKay                Cursor cursor = mScope.model.getItem(modelId);
5565a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay                if (modelId != null && cursor != null) {
5575a22a119246df6595c1e2eefa4ce9c5b0cb22841Steve McKay                    String title = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
5586fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    // Perform case-insensitive search.
5596fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    index.add(title.toLowerCase());
560472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                } else {
561472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                    index.add("");
562472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
563472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
564472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            mIndex = index;
565472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        }
566472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
567990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKay        private EventListener<Model.Update> mModelListener = new EventListener<Model.Update>() {
568472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            @Override
569990f76ea83a249cd8fc3c797e40626b94cd7945cSteve McKay            public void accept(Update event) {
570472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                // Invalidate the search index when the model updates.
571472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                mIndex = null;
572472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
573472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        };
574472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
5756fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private class TimeoutTask extends TimerTask {
5766fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            @Override
5776fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            public void run() {
5786fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                long last = mLastEvent.getEventTime();
5796fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                long now = SystemClock.uptimeMillis();
5806fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                if ((now - last) > SEARCH_TIMEOUT) {
5816fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    // endSearch must run on the main thread because it does UI work
582047182631669608af946480c2545a10acb2ef1bfSteve McKay                    mUiRunner.post(
583047182631669608af946480c2545a10acb2ef1bfSteve McKay                            new Runnable() {
584047182631669608af946480c2545a10acb2ef1bfSteve McKay                                @Override
585047182631669608af946480c2545a10acb2ef1bfSteve McKay                                public void run() {
586047182631669608af946480c2545a10acb2ef1bfSteve McKay                                    endSearch();
587047182631669608af946480c2545a10acb2ef1bfSteve McKay                                }
588047182631669608af946480c2545a10acb2ef1bfSteve McKay                            });
589472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
590472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
5916fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        };
5926fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa
5936fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa        private class Highlighter {
5946fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            private Spannable mCurrentHighlight;
595472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
596472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            /**
597047182631669608af946480c2545a10acb2ef1bfSteve McKay             * Applies title highlights to the given view. The view must have a title field that is
598047182631669608af946480c2545a10acb2ef1bfSteve McKay             * a spannable text field. If this condition is not met, this function does nothing.
5996fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa             *
6006fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa             * @param view
601472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa             */
6026fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            private void applyHighlight(View view) {
6036fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                TextView titleView = (TextView) view.findViewById(android.R.id.title);
6046fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                if (titleView == null) {
6056fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    return;
606472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
607472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
6086fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                CharSequence tmpText = titleView.getText();
6096fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                if (tmpText instanceof Spannable) {
6106fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    if (mCurrentHighlight != null) {
6116fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                        mCurrentHighlight.removeSpan(mSpan);
6126fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    }
6136fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    mCurrentHighlight = (Spannable) tmpText;
6146fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    mCurrentHighlight.setSpan(
6156fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                            mSpan, 0, mSearchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
6166fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                }
617472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
618472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa
6196fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            /**
620047182631669608af946480c2545a10acb2ef1bfSteve McKay             * Removes title highlights from the given view. The view must have a title field that
621047182631669608af946480c2545a10acb2ef1bfSteve McKay             * is a spannable text field. If this condition is not met, this function does nothing.
6226fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa             *
6236fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa             * @param view
6246fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa             */
6256fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa            private void removeHighlight() {
6266fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                if (mCurrentHighlight != null) {
6276fd431ee18b8d6ecaa2620229d3b40bcdf85e370Ben Kwa                    mCurrentHighlight.removeSpan(mSpan);
628472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa                }
629472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa            }
630472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa        };
631472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa    }
63217b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay
63317b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay    public FocusManager reset(RecyclerView view, Model model) {
6345b0a2c187a9e446b683687817d22cbe443585223Steve McKay        assert (view != null);
6355b0a2c187a9e446b683687817d22cbe443585223Steve McKay        assert (model != null);
6365b0a2c187a9e446b683687817d22cbe443585223Steve McKay        mScope.view = view;
6375b0a2c187a9e446b683687817d22cbe443585223Steve McKay        mScope.adapter = (DocumentsAdapter) view.getAdapter();
6385b0a2c187a9e446b683687817d22cbe443585223Steve McKay        mScope.layout = (GridLayoutManager) view.getLayoutManager();
6395b0a2c187a9e446b683687817d22cbe443585223Steve McKay        mScope.model = model;
64017b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay
6415b0a2c187a9e446b683687817d22cbe443585223Steve McKay        mScope.lastFocusPosition = RecyclerView.NO_POSITION;
6425b0a2c187a9e446b683687817d22cbe443585223Steve McKay        mScope.pendingFocusId = null;
64317b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay
6445b0a2c187a9e446b683687817d22cbe443585223Steve McKay        return this;
6455b0a2c187a9e446b683687817d22cbe443585223Steve McKay    }
64617b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay
6475b0a2c187a9e446b683687817d22cbe443585223Steve McKay    private static final class ContentScope {
6485b0a2c187a9e446b683687817d22cbe443585223Steve McKay        private @Nullable RecyclerView view;
6495b0a2c187a9e446b683687817d22cbe443585223Steve McKay        private @Nullable DocumentsAdapter adapter;
6505b0a2c187a9e446b683687817d22cbe443585223Steve McKay        private @Nullable GridLayoutManager layout;
6515b0a2c187a9e446b683687817d22cbe443585223Steve McKay        private @Nullable Model model;
65217b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay
6535b0a2c187a9e446b683687817d22cbe443585223Steve McKay        private @Nullable String pendingFocusId;
6545b0a2c187a9e446b683687817d22cbe443585223Steve McKay        private int lastFocusPosition = RecyclerView.NO_POSITION;
65517b761eb2f837d3ac079c07fc33877d6049c3cbaSteve McKay    }
65615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa}
657