blob: 07c992dc047dfdc3136f4c67253e1cab40083d11 [file] [log] [blame]
Ben Kwa15de7f92016-02-09 11:27:45 -08001/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.documentsui.dirlist;
18
Steve McKayd0805062016-09-15 14:30:38 -070019import static com.android.documentsui.base.DocumentInfo.getCursorString;
Ben Kwa472103f2016-02-10 15:48:25 -080020
Ben Lin81afd7f2016-08-22 18:24:03 -070021import android.annotation.ColorRes;
Ben Kwa6fd431e2016-02-23 23:00:01 -080022import android.annotation.Nullable;
Steve McKay5a22a112016-04-12 11:29:10 -070023import android.database.Cursor;
Ben Kwa6fd431e2016-02-23 23:00:01 -080024import android.os.Handler;
25import android.os.Looper;
26import android.os.SystemClock;
Ben Kwa472103f2016-02-10 15:48:25 -080027import android.provider.DocumentsContract.Document;
Ben Kwaa6c2f0a2016-02-10 11:54:55 -080028import android.support.v7.widget.GridLayoutManager;
Ben Kwa15de7f92016-02-09 11:27:45 -080029import android.support.v7.widget.RecyclerView;
Ben Kwa472103f2016-02-10 15:48:25 -080030import android.text.Editable;
31import android.text.Spannable;
32import android.text.method.KeyListener;
33import android.text.method.TextKeyListener;
34import android.text.method.TextKeyListener.Capitalize;
35import android.text.style.BackgroundColorSpan;
Ben Kwa15de7f92016-02-09 11:27:45 -080036import android.util.Log;
37import android.view.KeyEvent;
38import android.view.View;
Ben Kwa472103f2016-02-10 15:48:25 -080039import android.widget.TextView;
Ben Kwa15de7f92016-02-09 11:27:45 -080040
Steve McKay990f76e2016-09-16 12:36:58 -070041import com.android.documentsui.base.EventListener;
Steve McKayd9caa6a2016-09-15 16:36:45 -070042import com.android.documentsui.base.Events;
Steve McKay990f76e2016-09-16 12:36:58 -070043import com.android.documentsui.dirlist.Model.Update;
Ben Kwa472103f2016-02-10 15:48:25 -080044
45import java.util.ArrayList;
46import java.util.List;
Ben Kwa6fd431e2016-02-23 23:00:01 -080047import java.util.Timer;
48import java.util.TimerTask;
Ben Kwa15de7f92016-02-09 11:27:45 -080049
50/**
51 * A class that handles navigation and focus within the DirectoryFragment.
52 */
Ben Lin81afd7f2016-08-22 18:24:03 -070053public final class FocusManager implements FocusHandler {
Ben Kwa15de7f92016-02-09 11:27:45 -080054 private static final String TAG = "FocusManager";
55
56 private RecyclerView mView;
Ben Kwa472103f2016-02-10 15:48:25 -080057 private DocumentsAdapter mAdapter;
Ben Kwaa6c2f0a2016-02-10 11:54:55 -080058 private GridLayoutManager mLayout;
Ben Kwa15de7f92016-02-09 11:27:45 -080059
Ben Kwa472103f2016-02-10 15:48:25 -080060 private TitleSearchHelper mSearchHelper;
61 private Model mModel;
Ben Lin81afd7f2016-08-22 18:24:03 -070062 private @Nullable String mPendingFocusId;
Ben Kwa472103f2016-02-10 15:48:25 -080063
Ben Kwa74e5d412016-02-10 07:46:35 -080064 private int mLastFocusPosition = RecyclerView.NO_POSITION;
65
Ben Lin81afd7f2016-08-22 18:24:03 -070066 public FocusManager(RecyclerView view, Model model, @ColorRes int color) {
Ben Lin26819d72016-08-17 12:45:05 -070067 assert (view != null);
68 assert (model != null);
Ben Kwa15de7f92016-02-09 11:27:45 -080069 mView = view;
Ben Kwa472103f2016-02-10 15:48:25 -080070 mAdapter = (DocumentsAdapter) view.getAdapter();
Ben Kwaa6c2f0a2016-02-10 11:54:55 -080071 mLayout = (GridLayoutManager) view.getLayoutManager();
Ben Kwa472103f2016-02-10 15:48:25 -080072 mModel = model;
73
Ben Lin81afd7f2016-08-22 18:24:03 -070074 mSearchHelper = new TitleSearchHelper(color);
Ben Kwa15de7f92016-02-09 11:27:45 -080075 }
76
Steve McKay74956af2016-06-30 21:03:06 -070077 @Override
Ben Kwa15de7f92016-02-09 11:27:45 -080078 public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
Ben Kwa472103f2016-02-10 15:48:25 -080079 // Search helper gets first crack, for doing type-to-focus.
80 if (mSearchHelper.handleKey(doc, keyCode, event)) {
81 return true;
82 }
83
Ben Kwa92db0b32016-02-11 13:29:12 -080084 // Translate space/shift-space into PgDn/PgUp
85 if (keyCode == KeyEvent.KEYCODE_SPACE) {
86 if (event.isShiftPressed()) {
87 keyCode = KeyEvent.KEYCODE_PAGE_UP;
88 } else {
89 keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
90 }
Ben Kwa92db0b32016-02-11 13:29:12 -080091 }
92
Ben Kwa15de7f92016-02-09 11:27:45 -080093 if (Events.isNavigationKeyCode(keyCode)) {
94 // Find the target item and focus it.
95 int endPos = findTargetPosition(doc.itemView, keyCode, event);
96
97 if (endPos != RecyclerView.NO_POSITION) {
98 focusItem(endPos);
Ben Kwa15de7f92016-02-09 11:27:45 -080099 }
Ben Kwa74e5d412016-02-10 07:46:35 -0800100 // Swallow all navigation keystrokes. Otherwise they go to the app's global
101 // key-handler, which will route them back to the DF and cause focus to be reset.
102 return true;
Ben Kwa15de7f92016-02-09 11:27:45 -0800103 }
Ben Kwa74e5d412016-02-10 07:46:35 -0800104 return false;
105 }
106
107 @Override
108 public void onFocusChange(View v, boolean hasFocus) {
109 // Remember focus events on items.
110 if (hasFocus && v.getParent() == mView) {
111 mLastFocusPosition = mView.getChildAdapterPosition(v);
112 }
113 }
114
Steve McKay74956af2016-06-30 21:03:06 -0700115 @Override
Ben Kwa74e5d412016-02-10 07:46:35 -0800116 public void restoreLastFocus() {
Ben Kwa237432e2016-02-24 10:11:10 -0800117 if (mAdapter.getItemCount() == 0) {
118 // Nothing to focus.
119 return;
120 }
121
Ben Kwa74e5d412016-02-10 07:46:35 -0800122 if (mLastFocusPosition != RecyclerView.NO_POSITION) {
123 // The system takes care of situations when a view is no longer on screen, etc,
124 focusItem(mLastFocusPosition);
125 } else {
126 // Focus the first visible item
127 focusItem(mLayout.findFirstVisibleItemPosition());
128 }
Ben Kwa15de7f92016-02-09 11:27:45 -0800129 }
130
Ben Lin81afd7f2016-08-22 18:24:03 -0700131 /*
132 * Attempts to reset focus on the item corresponding to {@code mPendingFocusId} if it exists and
133 * has a valid position in the adapter. It then automatically resets {@code mPendingFocusId}.
134 */
135 @Override
136 public void onLayoutCompleted() {
137 if (mPendingFocusId == null) {
138 return;
139 }
140
141 int pos = mAdapter.getModelIds().indexOf(mPendingFocusId);
142 if (pos != -1) {
143 focusItem(pos);
144 }
145 mPendingFocusId = null;
146 }
147
148 /*
149 * Attempts to put focus on the document associated with the given modelId. If item does not
150 * exist yet in the layout, this sets a pending modelId to be used when
151 * {@code #applyPendingFocus()} is called next time.
152 */
153 @Override
154 public void onDirectoryCreated(String modelId) {
155 int pos = mAdapter.getModelIds().indexOf(modelId);
156 if (pos != -1 && mView.findViewHolderForAdapterPosition(pos) != null) {
157 focusItem(pos);
158 } else {
159 mPendingFocusId = modelId;
160 }
161 }
162
Steve McKay74956af2016-06-30 21:03:06 -0700163 @Override
Ben Kwa9504d762016-02-10 14:01:19 -0800164 public int getFocusPosition() {
165 return mLastFocusPosition;
166 }
167
168 /**
Ben Kwa15de7f92016-02-09 11:27:45 -0800169 * Finds the destination position where the focus should land for a given navigation event.
170 *
171 * @param view The view that received the event.
172 * @param keyCode The key code for the event.
173 * @param event
174 * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION.
175 */
176 private int findTargetPosition(View view, int keyCode, KeyEvent event) {
177 switch (keyCode) {
178 case KeyEvent.KEYCODE_MOVE_HOME:
179 return 0;
180 case KeyEvent.KEYCODE_MOVE_END:
181 return mAdapter.getItemCount() - 1;
182 case KeyEvent.KEYCODE_PAGE_UP:
183 case KeyEvent.KEYCODE_PAGE_DOWN:
184 return findPagedTargetPosition(view, keyCode, event);
185 }
186
187 // Find a navigation target based on the arrow key that the user pressed.
188 int searchDir = -1;
189 switch (keyCode) {
190 case KeyEvent.KEYCODE_DPAD_UP:
191 searchDir = View.FOCUS_UP;
192 break;
193 case KeyEvent.KEYCODE_DPAD_DOWN:
194 searchDir = View.FOCUS_DOWN;
195 break;
Ben Kwaa6c2f0a2016-02-10 11:54:55 -0800196 }
197
198 if (inGridMode()) {
199 int currentPosition = mView.getChildAdapterPosition(view);
200 // Left and right arrow keys only work in grid mode.
201 switch (keyCode) {
202 case KeyEvent.KEYCODE_DPAD_LEFT:
203 if (currentPosition > 0) {
204 // Stop backward focus search at the first item, otherwise focus will wrap
205 // around to the last visible item.
206 searchDir = View.FOCUS_BACKWARD;
207 }
208 break;
209 case KeyEvent.KEYCODE_DPAD_RIGHT:
210 if (currentPosition < mAdapter.getItemCount() - 1) {
211 // Stop forward focus search at the last item, otherwise focus will wrap
212 // around to the first visible item.
213 searchDir = View.FOCUS_FORWARD;
214 }
215 break;
216 }
Ben Kwa15de7f92016-02-09 11:27:45 -0800217 }
218
219 if (searchDir != -1) {
Ben Kwa67f06a32016-02-17 14:08:52 -0800220 // Focus search behaves badly if the parent RecyclerView is focused. However, focusable
221 // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after
222 // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable
223 // off while performing the focus search.
224 // TODO: Revisit this when RV focus issues are resolved.
225 mView.setFocusable(false);
Ben Kwa15de7f92016-02-09 11:27:45 -0800226 View targetView = view.focusSearch(searchDir);
Ben Kwa67f06a32016-02-17 14:08:52 -0800227 mView.setFocusable(true);
Ben Kwa15de7f92016-02-09 11:27:45 -0800228 // TargetView can be null, for example, if the user pressed <down> at the bottom
229 // of the list.
230 if (targetView != null) {
231 // Ignore navigation targets that aren't items in the RecyclerView.
232 if (targetView.getParent() == mView) {
233 return mView.getChildAdapterPosition(targetView);
234 }
235 }
236 }
237
238 return RecyclerView.NO_POSITION;
239 }
240
241 /**
242 * Given a PgUp/PgDn event and the current view, find the position of the target view.
243 * This returns:
244 * <li>The position of the topmost (or bottom-most) visible item, if the current item is not
245 * the top- or bottom-most visible item.
246 * <li>The position of an item that is one page's worth of items up (or down) if the current
247 * item is the top- or bottom-most visible item.
248 * <li>The first (or last) item, if paging up (or down) would go past those limits.
249 * @param view The view that received the key event.
250 * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN.
251 * @param event
252 * @return The adapter position of the target item.
253 */
254 private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) {
255 int first = mLayout.findFirstVisibleItemPosition();
256 int last = mLayout.findLastVisibleItemPosition();
257 int current = mView.getChildAdapterPosition(view);
258 int pageSize = last - first + 1;
259
260 if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
261 if (current > first) {
262 // If the current item isn't the first item, target the first item.
263 return first;
264 } else {
265 // If the current item is the first item, target the item one page up.
266 int target = current - pageSize;
267 return target < 0 ? 0 : target;
268 }
269 }
270
271 if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
272 if (current < last) {
273 // If the current item isn't the last item, target the last item.
274 return last;
275 } else {
276 // If the current item is the last item, target the item one page down.
277 int target = current + pageSize;
278 int max = mAdapter.getItemCount() - 1;
279 return target < max ? target : max;
280 }
281 }
282
283 throw new IllegalArgumentException("Unsupported keyCode: " + keyCode);
284 }
285
286 /**
287 * Requests focus for the item in the given adapter position, scrolling the RecyclerView if
288 * necessary.
289 *
290 * @param pos
291 */
292 private void focusItem(final int pos) {
Ben Kwa6fd431e2016-02-23 23:00:01 -0800293 focusItem(pos, null);
294 }
295
296 /**
297 * Requests focus for the item in the given adapter position, scrolling the RecyclerView if
298 * necessary.
299 *
300 * @param pos
301 * @param callback A callback to call after the given item has been focused.
302 */
303 private void focusItem(final int pos, @Nullable final FocusCallback callback) {
Ben Lin81afd7f2016-08-22 18:24:03 -0700304 if (mPendingFocusId != null) {
305 Log.v(TAG, "clearing pending focus id: " + mPendingFocusId);
306 mPendingFocusId = null;
307 }
308
Ben Kwa15de7f92016-02-09 11:27:45 -0800309 // If the item is already in view, focus it; otherwise, scroll to it and focus it.
310 RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
311 if (vh != null) {
Ben Kwa6fd431e2016-02-23 23:00:01 -0800312 if (vh.itemView.requestFocus() && callback != null) {
313 callback.onFocus(vh.itemView);
314 }
Ben Kwa15de7f92016-02-09 11:27:45 -0800315 } else {
Ben Kwa15de7f92016-02-09 11:27:45 -0800316 // Set a one-time listener to request focus when the scroll has completed.
317 mView.addOnScrollListener(
318 new RecyclerView.OnScrollListener() {
319 @Override
320 public void onScrollStateChanged(RecyclerView view, int newState) {
321 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
322 // When scrolling stops, find the item and focus it.
323 RecyclerView.ViewHolder vh =
324 view.findViewHolderForAdapterPosition(pos);
325 if (vh != null) {
Ben Kwa6fd431e2016-02-23 23:00:01 -0800326 if (vh.itemView.requestFocus() && callback != null) {
327 callback.onFocus(vh.itemView);
328 }
Ben Kwa15de7f92016-02-09 11:27:45 -0800329 } else {
330 // This might happen in weird corner cases, e.g. if the user is
331 // scrolling while a delete operation is in progress. In that
332 // case, just don't attempt to focus the missing item.
333 Log.w(TAG, "Unable to focus position " + pos + " after scroll");
334 }
335 view.removeOnScrollListener(this);
336 }
337 }
338 });
Ben Kwa472103f2016-02-10 15:48:25 -0800339 mView.smoothScrollToPosition(pos);
Ben Kwa15de7f92016-02-09 11:27:45 -0800340 }
341 }
Ben Kwaa6c2f0a2016-02-10 11:54:55 -0800342
343 /**
344 * @return Whether the layout manager is currently in a grid-configuration.
345 */
346 private boolean inGridMode() {
347 return mLayout.getSpanCount() > 1;
348 }
Ben Kwa472103f2016-02-10 15:48:25 -0800349
Ben Kwa6fd431e2016-02-23 23:00:01 -0800350 private interface FocusCallback {
351 public void onFocus(View view);
352 }
353
Ben Kwa472103f2016-02-10 15:48:25 -0800354 /**
355 * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via
356 * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build
357 * up a string from individual key events, and perform searching based on that string. When an
358 * item is found that matches the search term, that item will be focused. This class also
359 * highlights instances of the search term found in the view.
360 */
361 private class TitleSearchHelper {
Ben Kwa6fd431e2016-02-23 23:00:01 -0800362 static private final int SEARCH_TIMEOUT = 500; // ms
363
364 private final KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false);
365 private final Editable mSearchString = Editable.Factory.getInstance().newEditable("");
366 private final Highlighter mHighlighter = new Highlighter();
367 private final BackgroundColorSpan mSpan;
368
Ben Kwa472103f2016-02-10 15:48:25 -0800369 private List<String> mIndex;
370 private boolean mActive;
Ben Kwa6fd431e2016-02-23 23:00:01 -0800371 private Timer mTimer;
372 private KeyEvent mLastEvent;
373 private Handler mUiRunner;
Ben Kwa472103f2016-02-10 15:48:25 -0800374
Ben Lin81afd7f2016-08-22 18:24:03 -0700375 public TitleSearchHelper(@ColorRes int color) {
376 mSpan = new BackgroundColorSpan(color);
Ben Kwa6fd431e2016-02-23 23:00:01 -0800377 // Handler for running things on the main UI thread. Needed for updating the UI from a
378 // timer (see #activate, below).
379 mUiRunner = new Handler(Looper.getMainLooper());
Ben Kwa472103f2016-02-10 15:48:25 -0800380 }
381
382 /**
383 * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out
384 * of individual key events, and then performs a search for the given string.
385 *
386 * @param doc The document holder receiving the key event.
387 * @param keyCode
388 * @param event
389 * @return Whether the event was handled.
390 */
391 public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
392 switch (keyCode) {
393 case KeyEvent.KEYCODE_ESCAPE:
394 case KeyEvent.KEYCODE_ENTER:
395 if (mActive) {
396 // These keys end any active searches.
Ben Kwa6fd431e2016-02-23 23:00:01 -0800397 endSearch();
Ben Kwa472103f2016-02-10 15:48:25 -0800398 return true;
399 } else {
400 // Don't handle these key events if there is no active search.
401 return false;
402 }
403 case KeyEvent.KEYCODE_SPACE:
404 // This allows users to search for files with spaces in their names, but ignores
Ben Kwa6fd431e2016-02-23 23:00:01 -0800405 // spacebar events when a text search is not active. Ignoring the spacebar
406 // event is necessary because other handlers (see FocusManager#handleKey) also
407 // listen for and handle it.
Ben Kwa472103f2016-02-10 15:48:25 -0800408 if (!mActive) {
409 return false;
410 }
411 }
412
413 // Navigation keys also end active searches.
414 if (Events.isNavigationKeyCode(keyCode)) {
Ben Kwa6fd431e2016-02-23 23:00:01 -0800415 endSearch();
Ben Kwa472103f2016-02-10 15:48:25 -0800416 // Don't handle the keycode, so navigation still occurs.
417 return false;
418 }
419
420 // Build up the search string, and perform the search.
421 boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event);
422
423 // Delete is processed by the text listener, but not "handled". Check separately for it.
Ben Kwa6fd431e2016-02-23 23:00:01 -0800424 if (keyCode == KeyEvent.KEYCODE_DEL) {
425 handled = true;
426 }
427
428 if (handled) {
429 mLastEvent = event;
430 if (mSearchString.length() == 0) {
Ben Kwa472103f2016-02-10 15:48:25 -0800431 // Don't perform empty searches.
432 return false;
433 }
Ben Kwa6fd431e2016-02-23 23:00:01 -0800434 search();
Ben Kwa472103f2016-02-10 15:48:25 -0800435 }
436
437 return handled;
438 }
439
440 /**
441 * Activates the search helper, which changes its key handling and updates the search index
442 * and highlights if necessary. Call this each time the search term is updated.
443 */
Ben Kwa6fd431e2016-02-23 23:00:01 -0800444 private void search() {
Ben Kwa472103f2016-02-10 15:48:25 -0800445 if (!mActive) {
Ben Kwa6fd431e2016-02-23 23:00:01 -0800446 // The model listener invalidates the search index when the model changes.
Ben Kwa472103f2016-02-10 15:48:25 -0800447 mModel.addUpdateListener(mModelListener);
Ben Kwa6fd431e2016-02-23 23:00:01 -0800448
449 // Used to keep the current search alive until the timeout expires. If the user
450 // presses another key within that time, that keystroke is added to the current
451 // search. Otherwise, the current search ends, and subsequent keystrokes start a new
452 // search.
453 mTimer = new Timer();
454 mActive = true;
Ben Kwa472103f2016-02-10 15:48:25 -0800455 }
456
457 // If the search index was invalidated, rebuild it
458 if (mIndex == null) {
459 buildIndex();
460 }
461
Ben Kwa6fd431e2016-02-23 23:00:01 -0800462 // Search for the current search term.
463 // Perform case-insensitive search.
464 String searchString = mSearchString.toString().toLowerCase();
465 for (int pos = 0; pos < mIndex.size(); pos++) {
466 String title = mIndex.get(pos);
467 if (title != null && title.startsWith(searchString)) {
468 focusItem(pos, new FocusCallback() {
469 @Override
470 public void onFocus(View view) {
471 mHighlighter.applyHighlight(view);
472 // Using a timer repeat period of SEARCH_TIMEOUT/2 means the amount of
473 // time between the last keystroke and a search expiring is actually
474 // between 500 and 750 ms. A smaller timer period results in less
475 // variability but does more polling.
476 mTimer.schedule(new TimeoutTask(), 0, SEARCH_TIMEOUT / 2);
477 }
478 });
479 break;
Ben Kwa472103f2016-02-10 15:48:25 -0800480 }
481 }
482 }
483
484 /**
Ben Kwa6fd431e2016-02-23 23:00:01 -0800485 * Ends the current search (see {@link #search()}.
Ben Kwa472103f2016-02-10 15:48:25 -0800486 */
Ben Kwa6fd431e2016-02-23 23:00:01 -0800487 private void endSearch() {
488 if (mActive) {
489 mModel.removeUpdateListener(mModelListener);
490 mTimer.cancel();
Ben Kwa472103f2016-02-10 15:48:25 -0800491 }
492
Ben Kwa6fd431e2016-02-23 23:00:01 -0800493 mHighlighter.removeHighlight();
494
495 mIndex = null;
496 mSearchString.clear();
497 mActive = false;
Ben Kwa472103f2016-02-10 15:48:25 -0800498 }
499
500 /**
501 * Builds a search index for finding items by title. Queries the model and adapter, so both
502 * must be set up before calling this method.
503 */
504 private void buildIndex() {
505 int itemCount = mAdapter.getItemCount();
506 List<String> index = new ArrayList<>(itemCount);
507 for (int i = 0; i < itemCount; i++) {
508 String modelId = mAdapter.getModelId(i);
Steve McKay5a22a112016-04-12 11:29:10 -0700509 Cursor cursor = mModel.getItem(modelId);
510 if (modelId != null && cursor != null) {
511 String title = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
Ben Kwa6fd431e2016-02-23 23:00:01 -0800512 // Perform case-insensitive search.
513 index.add(title.toLowerCase());
Ben Kwa472103f2016-02-10 15:48:25 -0800514 } else {
515 index.add("");
516 }
517 }
518 mIndex = index;
519 }
520
Steve McKay990f76e2016-09-16 12:36:58 -0700521 private EventListener<Model.Update> mModelListener = new EventListener<Model.Update>() {
Ben Kwa472103f2016-02-10 15:48:25 -0800522 @Override
Steve McKay990f76e2016-09-16 12:36:58 -0700523 public void accept(Update event) {
Ben Kwa472103f2016-02-10 15:48:25 -0800524 // Invalidate the search index when the model updates.
525 mIndex = null;
526 }
527 };
528
Ben Kwa6fd431e2016-02-23 23:00:01 -0800529 private class TimeoutTask extends TimerTask {
530 @Override
531 public void run() {
532 long last = mLastEvent.getEventTime();
533 long now = SystemClock.uptimeMillis();
534 if ((now - last) > SEARCH_TIMEOUT) {
535 // endSearch must run on the main thread because it does UI work
536 mUiRunner.post(new Runnable() {
537 @Override
538 public void run() {
539 endSearch();
540 }
541 });
Ben Kwa472103f2016-02-10 15:48:25 -0800542 }
Ben Kwa472103f2016-02-10 15:48:25 -0800543 }
Ben Kwa6fd431e2016-02-23 23:00:01 -0800544 };
545
546 private class Highlighter {
547 private Spannable mCurrentHighlight;
Ben Kwa472103f2016-02-10 15:48:25 -0800548
549 /**
Ben Kwa6fd431e2016-02-23 23:00:01 -0800550 * Applies title highlights to the given view. The view must have a title field that is a
551 * spannable text field. If this condition is not met, this function does nothing.
552 *
553 * @param view
Ben Kwa472103f2016-02-10 15:48:25 -0800554 */
Ben Kwa6fd431e2016-02-23 23:00:01 -0800555 private void applyHighlight(View view) {
Ben Kwa472103f2016-02-10 15:48:25 -0800556 TextView titleView = (TextView) view.findViewById(android.R.id.title);
Ben Kwa6fd431e2016-02-23 23:00:01 -0800557 if (titleView == null) {
558 return;
559 }
560
561 CharSequence tmpText = titleView.getText();
562 if (tmpText instanceof Spannable) {
563 if (mCurrentHighlight != null) {
564 mCurrentHighlight.removeSpan(mSpan);
565 }
566 mCurrentHighlight = (Spannable) tmpText;
567 mCurrentHighlight.setSpan(
568 mSpan, 0, mSearchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
569 }
570 }
571
572 /**
573 * Removes title highlights from the given view. The view must have a title field that is a
574 * spannable text field. If this condition is not met, this function does nothing.
575 *
576 * @param view
577 */
578 private void removeHighlight() {
579 if (mCurrentHighlight != null) {
580 mCurrentHighlight.removeSpan(mSpan);
Ben Kwa472103f2016-02-10 15:48:25 -0800581 }
582 }
583 };
584 }
Ben Kwa15de7f92016-02-09 11:27:45 -0800585}