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