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