SearchActivityView.java revision 2cbf1d099dbd04c612e3cba34a70293705730fa0
1/* 2 * Copyright (C) 2010 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.quicksearchbox.ui; 18 19import com.android.quicksearchbox.Corpora; 20import com.android.quicksearchbox.Corpus; 21import com.android.quicksearchbox.CorpusResult; 22import com.android.quicksearchbox.Logger; 23import com.android.quicksearchbox.QsbApplication; 24import com.android.quicksearchbox.R; 25import com.android.quicksearchbox.SearchActivity; 26import com.android.quicksearchbox.SuggestionCursor; 27import com.android.quicksearchbox.Suggestions; 28import com.android.quicksearchbox.VoiceSearch; 29import com.android.quicksearchbox.ui.SuggestionsAdapter.SuggestionsAdapterChangeListener; 30 31import android.app.Activity; 32import android.content.Context; 33import android.database.DataSetObserver; 34import android.graphics.drawable.Drawable; 35import android.text.Editable; 36import android.text.TextWatcher; 37import android.util.AttributeSet; 38import android.util.Log; 39import android.view.KeyEvent; 40import android.view.View; 41import android.view.inputmethod.CompletionInfo; 42import android.view.inputmethod.InputMethodManager; 43import android.widget.AbsListView; 44import android.widget.ImageButton; 45import android.widget.RelativeLayout; 46 47import java.util.ArrayList; 48import java.util.Arrays; 49 50/** 51 * 52 */ 53public abstract class SearchActivityView extends RelativeLayout 54 implements SuggestionsAdapterChangeListener { 55 protected static final boolean DBG = false; 56 protected static final String TAG = "QSB.SearchActivityView"; 57 58 // The string used for privateImeOptions to identify to the IME that it should not show 59 // a microphone button since one already exists in the search dialog. 60 // TODO: This should move to android-common or something. 61 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 62 63 protected QueryTextView mQueryTextView; 64 // True if the query was empty on the previous call to updateQuery() 65 protected boolean mQueryWasEmpty = true; 66 protected Drawable mQueryTextEmptyBg; 67 protected Drawable mQueryTextNotEmptyBg; 68 69 protected SuggestionsView mSuggestionsView; 70 71 protected SuggestionsAdapter mSuggestionsAdapter; 72 73 protected ImageButton mSearchCloseButton; 74 protected ImageButton mSearchGoButton; 75 protected ImageButton mVoiceSearchButton; 76 77 protected ButtonsKeyListener mButtonsKeyListener; 78 79 private boolean mUpdateSuggestions; 80 81 private QueryListener mQueryListener; 82 private SearchClickListener mSearchClickListener; 83 84 public SearchActivityView(Context context) { 85 super(context); 86 } 87 88 public SearchActivityView(Context context, AttributeSet attrs) { 89 super(context, attrs); 90 } 91 92 public SearchActivityView(Context context, AttributeSet attrs, int defStyle) { 93 super(context, attrs, defStyle); 94 } 95 96 @Override 97 protected void onFinishInflate() { 98 mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text); 99 100 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 101 mSuggestionsView.setOnScrollListener(new InputMethodCloser()); 102 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 103 mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); 104 105 mSuggestionsAdapter = createSuggestionsAdapter(); 106 mSuggestionsAdapter.setSuggestionAdapterChangeListener(this); 107 // TODO: why do we need focus listeners both on the SuggestionsView and the individual 108 // suggestions? 109 mSuggestionsAdapter.setOnFocusChangeListener(new SuggestListFocusListener()); 110 111 mSearchCloseButton = (ImageButton) findViewById(R.id.search_close_btn); 112 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 113 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 114 115 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 116 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 117 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 118 mQueryTextEmptyBg = mQueryTextView.getBackground(); 119 120 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 121 122 mButtonsKeyListener = new ButtonsKeyListener(); 123 mSearchGoButton.setOnKeyListener(mButtonsKeyListener); 124 mVoiceSearchButton.setOnKeyListener(mButtonsKeyListener); 125 if (mSearchCloseButton != null) { 126 mSearchCloseButton.setOnKeyListener(mButtonsKeyListener); 127 mSearchCloseButton.setOnClickListener(new CloseClickListener()); 128 } 129 130 mUpdateSuggestions = true; 131 } 132 133 public void onSuggestionAdapterChanged() { 134 mSuggestionsView.setAdapter(mSuggestionsAdapter); 135 } 136 137 public abstract void onResume(); 138 139 public abstract void onStop(); 140 141 public void start() { 142 mSuggestionsAdapter.registerDataSetObserver(new SuggestionsObserver()); 143 mSuggestionsView.setAdapter(mSuggestionsAdapter); 144 } 145 146 public void destroy() { 147 mSuggestionsAdapter.setSuggestionAdapterChangeListener(null); 148 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 149 } 150 151 public void registerSuggestionsObserver(DataSetObserver observer) { 152 mSuggestionsAdapter.registerDataSetObserver(observer); 153 } 154 155 public void unregisterSuggestionsObserver(DataSetObserver observer) { 156 mSuggestionsAdapter.unregisterDataSetObserver(observer); 157 } 158 159 // TODO: Get rid of this. To make it more easily testable, 160 // the SearchActivityView should not depend on QsbApplication. 161 protected QsbApplication getQsbApplication() { 162 return QsbApplication.get(getContext()); 163 } 164 165 protected CorpusViewFactory getCorpusViewFactory() { 166 return getQsbApplication().getCorpusViewFactory(); 167 } 168 169 private VoiceSearch getVoiceSearch() { 170 return getQsbApplication().getVoiceSearch(); 171 } 172 173 protected SuggestionsAdapter createSuggestionsAdapter() { 174 return new DelayingSuggestionsAdapter(getQsbApplication().getDefaultSuggestionViewFactory(), 175 getQsbApplication().getCorpora()); 176 } 177 178 179 protected Corpora getCorpora() { 180 return getQsbApplication().getCorpora(); 181 } 182 183 public abstract void setCorpus(Corpus corpus); 184 185 public abstract Corpus getCorpus(); 186 187 protected Corpus getCorpus(String sourceName) { 188 if (sourceName == null) return null; 189 Corpus corpus = getCorpora().getCorpus(sourceName); 190 if (corpus == null) { 191 Log.w(TAG, "Unknown corpus " + sourceName); 192 return null; 193 } 194 return corpus; 195 } 196 197 public void setCorpus(String corpusName) { 198 if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")"); 199 Corpus corpus = getCorpus(corpusName); 200 setCorpus(corpus); 201 } 202 203 public String getCorpusName() { 204 return getCorpus() == null ? null : getCorpus().getName(); 205 } 206 207 public abstract Corpus getSearchCorpus(); 208 209 public Corpus getWebCorpus() { 210 Corpus webCorpus = getCorpora().getWebCorpus(); 211 if (webCorpus == null) { 212 Log.e(TAG, "No web corpus"); 213 } 214 return webCorpus; 215 } 216 217 public void setMaxPromoted(int maxPromoted) { 218 mSuggestionsAdapter.setMaxPromoted(maxPromoted); 219 } 220 221 public void setQueryListener(QueryListener listener) { 222 mQueryListener = listener; 223 } 224 225 public void setSearchClickListener(SearchClickListener listener) { 226 mSearchClickListener = listener; 227 } 228 229 public abstract void showCorpusSelectionDialog(); 230 231 public abstract void setSettingsButtonClickListener(View.OnClickListener listener); 232 233 public void setVoiceSearchButtonClickListener(View.OnClickListener listener) { 234 if (mVoiceSearchButton != null) { 235 mVoiceSearchButton.setOnClickListener(listener); 236 } 237 } 238 239 public void setSuggestionClickListener(final SuggestionClickListener listener) { 240 mSuggestionsAdapter.setSuggestionClickListener(listener); 241 mQueryTextView.setCommitCompletionListener(new QueryTextView.CommitCompletionListener() { 242 @Override 243 public void onCommitCompletion(int position) { 244 mSuggestionsAdapter.onSuggestionClicked(position); 245 } 246 }); 247 } 248 249 protected SuggestionsAdapter getSuggestionsAdapter() { 250 return mSuggestionsAdapter; 251 } 252 253 public Suggestions getSuggestions() { 254 return mSuggestionsAdapter.getSuggestions(); 255 } 256 257 public SuggestionCursor getCurrentSuggestions() { 258 return mSuggestionsAdapter.getCurrentSuggestions(); 259 } 260 261 public void setSuggestions(Suggestions suggestions) { 262 suggestions.acquire(); 263 mSuggestionsAdapter.setSuggestions(suggestions); 264 } 265 266 public void clearSuggestions() { 267 mSuggestionsAdapter.setSuggestions(null); 268 } 269 270 public String getQuery() { 271 CharSequence q = mQueryTextView.getText(); 272 return q == null ? "" : q.toString(); 273 } 274 275 /** 276 * Sets the text in the query box. Does not update the suggestions. 277 */ 278 public void setQuery(String query, boolean selectAll) { 279 mUpdateSuggestions = false; 280 mQueryTextView.setText(query); 281 mQueryTextView.setTextSelection(selectAll); 282 mUpdateSuggestions = true; 283 } 284 285 protected SearchActivity getActivity() { 286 Context context = getContext(); 287 if (context instanceof SearchActivity) { 288 return (SearchActivity) context; 289 } else { 290 return null; 291 } 292 } 293 294 public void focusQueryTextView() { 295 mQueryTextView.requestFocus(); 296 } 297 298 protected void updateUi(boolean queryEmpty) { 299 updateQueryTextView(queryEmpty); 300 updateSearchGoButton(queryEmpty); 301 updateVoiceSearchButton(queryEmpty); 302 } 303 304 private void updateQueryTextView(boolean queryEmpty) { 305 if (queryEmpty) { 306 if (isSearchCorpusWeb()) { 307 mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg); 308 mQueryTextView.setHint(null); 309 } else { 310 if (mQueryTextNotEmptyBg == null) { 311 mQueryTextNotEmptyBg = 312 getResources().getDrawable(R.drawable.textfield_search_empty); 313 } 314 mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg); 315 Corpus corpus = getCorpus(); 316 mQueryTextView.setHint(corpus == null ? "" : corpus.getHint()); 317 } 318 } else { 319 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 320 } 321 } 322 323 private void updateSearchGoButton(boolean queryEmpty) { 324 if (queryEmpty) { 325 mSearchGoButton.setVisibility(View.GONE); 326 } else { 327 mSearchGoButton.setVisibility(View.VISIBLE); 328 } 329 } 330 331 protected void updateVoiceSearchButton(boolean queryEmpty) { 332 if (queryEmpty && getVoiceSearch().shouldShowVoiceSearch(getCorpus())) { 333 mVoiceSearchButton.setVisibility(View.VISIBLE); 334 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 335 } else { 336 mVoiceSearchButton.setVisibility(View.GONE); 337 mQueryTextView.setPrivateImeOptions(null); 338 } 339 } 340 341 /** 342 * Hides the input method. 343 */ 344 protected void hideInputMethod() { 345 InputMethodManager imm = (InputMethodManager) 346 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 347 if (imm != null) { 348 imm.hideSoftInputFromWindow(getWindowToken(), 0); 349 } 350 } 351 352 protected abstract void considerHidingInputMethod(); 353 354 public void showInputMethodForQuery() { 355 mQueryTextView.showInputMethod(); 356 } 357 358 /** 359 * Overrides the handling of the back key to dismiss the activity. 360 */ 361 @Override 362 public boolean dispatchKeyEventPreIme(KeyEvent event) { 363 Activity activity = getActivity(); 364 if (activity != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 365 KeyEvent.DispatcherState state = getKeyDispatcherState(); 366 if (state != null) { 367 if (event.getAction() == KeyEvent.ACTION_DOWN 368 && event.getRepeatCount() == 0) { 369 state.startTracking(event, this); 370 return true; 371 } else if (event.getAction() == KeyEvent.ACTION_UP 372 && !event.isCanceled() && state.isTracking(event)) { 373 hideInputMethod(); 374 activity.onBackPressed(); 375 return true; 376 } 377 } 378 } 379 return super.dispatchKeyEventPreIme(event); 380 } 381 382 /** 383 * If the input method is in fullscreen mode, and the selector corpus 384 * is All or Web, use the web search suggestions as completions. 385 */ 386 protected void updateInputMethodSuggestions() { 387 InputMethodManager imm = (InputMethodManager) 388 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 389 if (imm == null || !imm.isFullscreenMode()) return; 390 Suggestions suggestions = mSuggestionsAdapter.getSuggestions(); 391 if (suggestions == null) return; 392 CompletionInfo[] completions = webSuggestionsToCompletions(suggestions); 393 if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")"); 394 imm.displayCompletions(mQueryTextView, completions); 395 } 396 397 private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) { 398 // TODO: This should also include include web search shortcuts 399 CorpusResult cursor = suggestions.getWebResult(); 400 if (cursor == null) return null; 401 int count = cursor.getCount(); 402 ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count); 403 boolean usingWebCorpus = isSearchCorpusWeb(); 404 for (int i = 0; i < count; i++) { 405 cursor.moveTo(i); 406 if (!usingWebCorpus || cursor.isWebSearchSuggestion()) { 407 String text1 = cursor.getSuggestionText1(); 408 completions.add(new CompletionInfo(i, i, text1)); 409 } 410 } 411 return completions.toArray(new CompletionInfo[completions.size()]); 412 } 413 414 /** 415 * Checks if the corpus used for typed searches is the web corpus. 416 */ 417 protected boolean isSearchCorpusWeb() { 418 Corpus corpus = getSearchCorpus(); 419 return corpus != null && corpus.isWebCorpus(); 420 } 421 422 protected boolean onSuggestionKeyDown(SuggestionsAdapter adapter, 423 int position, int keyCode, KeyEvent event) { 424 // Treat enter or search as a click 425 if ( keyCode == KeyEvent.KEYCODE_ENTER 426 || keyCode == KeyEvent.KEYCODE_SEARCH 427 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 428 if (adapter != null) { 429 SuggestionsAdapter suggestionsAdapter = adapter; 430 suggestionsAdapter.onSuggestionClicked(position); 431 return true; 432 } else { 433 return false; 434 } 435 } 436 437 return false; 438 } 439 440 protected boolean onSearchClicked(int method) { 441 if (mSearchClickListener != null) { 442 return mSearchClickListener.onSearchClicked(method); 443 } 444 return false; 445 } 446 447 /** 448 * Filters the suggestions list when the search text changes. 449 */ 450 private class SearchTextWatcher implements TextWatcher { 451 public void afterTextChanged(Editable s) { 452 boolean empty = s.length() == 0; 453 if (empty != mQueryWasEmpty) { 454 mQueryWasEmpty = empty; 455 updateUi(empty); 456 } 457 if (mUpdateSuggestions) { 458 if (mQueryListener != null) { 459 mQueryListener.onQueryChanged(); 460 } 461 } 462 } 463 464 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 465 } 466 467 public void onTextChanged(CharSequence s, int start, int before, int count) { 468 } 469 } 470 471 /** 472 * Handles key events on the suggestions list view. 473 */ 474 protected class SuggestionsViewKeyListener implements View.OnKeyListener { 475 public boolean onKey(View v, int keyCode, KeyEvent event) { 476 if (event.getAction() == KeyEvent.ACTION_DOWN 477 && v instanceof SuggestionsView) { 478 SuggestionsView view = ((SuggestionsView) v); 479 int position = view.getSelectedPosition(); 480 if (onSuggestionKeyDown(view.getAdapter(), position, keyCode, event)) { 481 return true; 482 } 483 } 484 return forwardKeyToQueryTextView(keyCode, event); 485 } 486 } 487 488 private class InputMethodCloser implements SuggestionsView.OnScrollListener { 489 490 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 491 int totalItemCount) { 492 } 493 494 public void onScrollStateChanged(AbsListView view, int scrollState) { 495 considerHidingInputMethod(); 496 } 497 } 498 499 /** 500 * Listens for clicks on the source selector. 501 */ 502 private class SearchGoButtonClickListener implements View.OnClickListener { 503 public void onClick(View view) { 504 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 505 } 506 } 507 508 /** 509 * Handles non-text keys in the query text view. 510 */ 511 private class QueryTextViewKeyListener implements View.OnKeyListener { 512 public boolean onKey(View view, int keyCode, KeyEvent event) { 513 // Handle IME search action key 514 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 515 // if no action was taken, consume the key event so that the keyboard 516 // remains on screen. 517 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 518 } 519 return false; 520 } 521 } 522 523 /** 524 * Handles key events on the search and voice search buttons, 525 * by refocusing to EditText. 526 */ 527 private class ButtonsKeyListener implements View.OnKeyListener { 528 public boolean onKey(View v, int keyCode, KeyEvent event) { 529 return forwardKeyToQueryTextView(keyCode, event); 530 } 531 } 532 533 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 534 if (!event.isSystem() && !isDpadKey(keyCode)) { 535 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 536 if (mQueryTextView.requestFocus()) { 537 return mQueryTextView.dispatchKeyEvent(event); 538 } 539 } 540 return false; 541 } 542 543 private boolean isDpadKey(int keyCode) { 544 switch (keyCode) { 545 case KeyEvent.KEYCODE_DPAD_UP: 546 case KeyEvent.KEYCODE_DPAD_DOWN: 547 case KeyEvent.KEYCODE_DPAD_LEFT: 548 case KeyEvent.KEYCODE_DPAD_RIGHT: 549 case KeyEvent.KEYCODE_DPAD_CENTER: 550 return true; 551 default: 552 return false; 553 } 554 } 555 556 /** 557 * Hides the input method when the suggestions get focus. 558 */ 559 private class SuggestListFocusListener implements OnFocusChangeListener { 560 public void onFocusChange(View v, boolean focused) { 561 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 562 if (focused) { 563 considerHidingInputMethod(); 564 } 565 } 566 } 567 568 private class QueryTextViewFocusListener implements OnFocusChangeListener { 569 public void onFocusChange(View v, boolean focused) { 570 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 571 if (focused) { 572 // The query box got focus, show the input method 573 showInputMethodForQuery(); 574 } 575 } 576 } 577 578 private class SuggestionsObserver extends DataSetObserver { 579 @Override 580 public void onChanged() { 581 updateInputMethodSuggestions(); 582 } 583 } 584 585 public interface QueryListener { 586 void onQueryChanged(); 587 } 588 589 public interface SearchClickListener { 590 boolean onSearchClicked(int method); 591 } 592 593 private class CloseClickListener implements OnClickListener { 594 595 public void onClick(View v) { 596 mQueryTextView.setText(""); 597 } 598 } 599} 600