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