SearchFragment.java revision 7e599cd800f063eb6c7f965d5f13c7ae0556be1d
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.app; 15 16import android.app.Fragment; 17import android.content.Intent; 18import android.graphics.drawable.Drawable; 19import android.os.Bundle; 20import android.os.Handler; 21import android.os.Message; 22import android.speech.SpeechRecognizer; 23import android.speech.RecognizerIntent; 24import android.support.v17.leanback.widget.ObjectAdapter; 25import android.support.v17.leanback.widget.ObjectAdapter.DataObserver; 26import android.support.v17.leanback.widget.OnItemClickedListener; 27import android.support.v17.leanback.widget.OnItemSelectedListener; 28import android.support.v17.leanback.widget.OnItemViewClickedListener; 29import android.support.v17.leanback.widget.OnItemViewSelectedListener; 30import android.support.v17.leanback.widget.Row; 31import android.support.v17.leanback.widget.RowPresenter; 32import android.support.v17.leanback.widget.SearchBar; 33import android.support.v17.leanback.widget.VerticalGridView; 34import android.support.v17.leanback.widget.Presenter.ViewHolder; 35import android.support.v17.leanback.widget.SpeechRecognitionCallback; 36import android.util.Log; 37import android.view.LayoutInflater; 38import android.view.View; 39import android.view.ViewGroup; 40import android.widget.FrameLayout; 41import android.support.v17.leanback.R; 42 43import java.util.ArrayList; 44import java.util.List; 45 46/** 47 * A fragment to handle searches. An application will supply an implementation 48 * of the {@link SearchResultProvider} interface to handle the search and return 49 * an {@link ObjectAdapter} containing the results. The results are rendered 50 * into a {@link RowsFragment}, in the same way that they are in a {@link 51 * BrowseFragment}. 52 * 53 * <p>If you do not supply a callback via 54 * {@link #setSpeechRecognitionCallback(SpeechRecognitionCallback)}, an internal speech 55 * recognizer will be used for which your application will need to request 56 * android.permission.RECORD_AUDIO. 57 */ 58public class SearchFragment extends Fragment { 59 private static final String TAG = SearchFragment.class.getSimpleName(); 60 private static final boolean DEBUG = false; 61 62 private static final String ARG_PREFIX = SearchFragment.class.getCanonicalName(); 63 private static final String ARG_QUERY = ARG_PREFIX + ".query"; 64 private static final String ARG_TITLE = ARG_PREFIX + ".title"; 65 66 private static final int MSG_DESTROY_RECOGNIZER = 1; 67 68 /** 69 * Search API to be provided by the application. 70 */ 71 public static interface SearchResultProvider { 72 /** 73 * <p>Method invoked some time prior to the first call to onQueryTextChange to retrieve 74 * an ObjectAdapter that will contain the results to future updates of the search query.</p> 75 * 76 * <p>As results are retrieved, the application should use the data set notification methods 77 * on the ObjectAdapter to instruct the SearchFragment to update the results.</p> 78 * 79 * @return ObjectAdapter The result object adapter. 80 */ 81 public ObjectAdapter getResultsAdapter(); 82 83 /** 84 * <p>Method invoked when the search query is updated.</p> 85 * 86 * <p>This is called as soon as the query changes; it is up to the application to add a 87 * delay before actually executing the queries if needed. 88 * 89 * <p>This method might not always be called before onQueryTextSubmit gets called, in 90 * particular for voice input. 91 * 92 * @param newQuery The current search query. 93 * @return whether the results changed as a result of the new query. 94 */ 95 public boolean onQueryTextChange(String newQuery); 96 97 /** 98 * Method invoked when the search query is submitted, either by dismissing the keyboard, 99 * pressing search or next on the keyboard or when voice has detected the end of the query. 100 * 101 * @param query The query entered. 102 * @return whether the results changed as a result of the query. 103 */ 104 public boolean onQueryTextSubmit(String query); 105 } 106 107 private final DataObserver mAdapterObserver = new DataObserver() { 108 public void onChanged() { 109 resultsChanged(); 110 } 111 }; 112 113 private RowsFragment mRowsFragment; 114 private final Handler mHandler = new Handler() { 115 @Override 116 public void handleMessage (Message msg) { 117 switch (msg.what) { 118 case MSG_DESTROY_RECOGNIZER: 119 if (null != mSpeechRecognizer) { 120 if (DEBUG) Log.v(TAG, "Destroy recognizer"); 121 mSpeechRecognizer.destroy(); 122 mSpeechRecognizer = null; 123 } 124 } 125 } 126 }; 127 128 private SearchBar mSearchBar; 129 private SearchResultProvider mProvider; 130 private String mPendingQuery = null; 131 132 private OnItemSelectedListener mOnItemSelectedListener; 133 private OnItemClickedListener mOnItemClickedListener; 134 private OnItemViewSelectedListener mOnItemViewSelectedListener; 135 private OnItemViewClickedListener mOnItemViewClickedListener; 136 private ObjectAdapter mResultAdapter; 137 private SpeechRecognitionCallback mSpeechRecognitionCallback; 138 139 private String mTitle; 140 private Drawable mBadgeDrawable; 141 142 private SpeechRecognizer mSpeechRecognizer; 143 144 private final int RESULTS_CHANGED = 0x1; 145 private final int QUERY_COMPLETE = 0x2; 146 147 private int mStatus; 148 149 /** 150 * @param args Bundle to use for the arguments, if null a new Bundle will be created. 151 */ 152 public static Bundle createArgs(Bundle args, String query) { 153 return createArgs(args, query, null); 154 } 155 156 public static Bundle createArgs(Bundle args, String query, String title) { 157 if (args == null) { 158 args = new Bundle(); 159 } 160 args.putString(ARG_QUERY, query); 161 args.putString(ARG_TITLE, title); 162 return args; 163 } 164 165 /** 166 * Create a search fragment with a given search query. 167 * 168 * <p>You should only use this if you need to start the search fragment with a 169 * pre-filled query. 170 * 171 * @param query The search query to begin with. 172 * @return A new SearchFragment. 173 */ 174 public static SearchFragment newInstance(String query) { 175 SearchFragment fragment = new SearchFragment(); 176 Bundle args = createArgs(null, query); 177 fragment.setArguments(args); 178 return fragment; 179 } 180 181 @Override 182 public void onCreate(Bundle savedInstanceState) { 183 super.onCreate(savedInstanceState); 184 } 185 186 @Override 187 public View onCreateView(LayoutInflater inflater, ViewGroup container, 188 Bundle savedInstanceState) { 189 View root = inflater.inflate(R.layout.lb_search_fragment, container, false); 190 191 FrameLayout searchFrame = (FrameLayout) root.findViewById(R.id.lb_search_frame); 192 mSearchBar = (SearchBar) searchFrame.findViewById(R.id.lb_search_bar); 193 mSearchBar.setSearchBarListener(new SearchBar.SearchBarListener() { 194 @Override 195 public void onSearchQueryChange(String query) { 196 if (DEBUG) Log.v(TAG, String.format("onSearchQueryChange %s", query)); 197 if (null != mProvider) { 198 retrieveResults(query); 199 } else { 200 mPendingQuery = query; 201 } 202 } 203 204 @Override 205 public void onSearchQuerySubmit(String query) { 206 if (DEBUG) Log.v(TAG, String.format("onSearchQuerySubmit %s", query)); 207 queryComplete(); 208 if (null != mProvider) { 209 mProvider.onQueryTextSubmit(query); 210 } 211 } 212 213 @Override 214 public void onKeyboardDismiss(String query) { 215 if (DEBUG) Log.v(TAG, String.format("onKeyboardDismiss %s", query)); 216 queryComplete(); 217 } 218 }); 219 mSearchBar.setSpeechRecognitionCallback(mSpeechRecognitionCallback); 220 221 readArguments(getArguments()); 222 if (null != mBadgeDrawable) { 223 setBadgeDrawable(mBadgeDrawable); 224 } 225 if (null != mTitle) { 226 setTitle(mTitle); 227 } 228 229 // Inject the RowsFragment in the results container 230 if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) { 231 mRowsFragment = new RowsFragment(); 232 getChildFragmentManager().beginTransaction() 233 .replace(R.id.lb_results_frame, mRowsFragment).commit(); 234 } else { 235 mRowsFragment = (RowsFragment) getChildFragmentManager() 236 .findFragmentById(R.id.browse_container_dock); 237 } 238 mRowsFragment.setOnItemViewSelectedListener(new OnItemViewSelectedListener() { 239 @Override 240 public void onItemSelected(ViewHolder itemViewHolder, Object item, 241 RowPresenter.ViewHolder rowViewHolder, Row row) { 242 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 243 if (DEBUG) Log.v(TAG, String.format("onItemSelected %d", position)); 244 mSearchBar.setVisibility(0 >= position ? View.VISIBLE : View.GONE); 245 if (null != mOnItemSelectedListener) { 246 mOnItemSelectedListener.onItemSelected(item, row); 247 } 248 if (null != mOnItemViewSelectedListener) { 249 mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 250 rowViewHolder, row); 251 } 252 } 253 }); 254 mRowsFragment.setOnItemViewClickedListener(new OnItemViewClickedListener() { 255 @Override 256 public void onItemClicked(ViewHolder itemViewHolder, Object item, 257 RowPresenter.ViewHolder rowViewHolder, Row row) { 258 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 259 if (DEBUG) Log.v(TAG, String.format("onItemClicked %d", position)); 260 if (null != mOnItemClickedListener) { 261 mOnItemClickedListener.onItemClicked(item, row); 262 } 263 if (null != mOnItemViewClickedListener) { 264 mOnItemViewClickedListener.onItemClicked(itemViewHolder, item, 265 rowViewHolder, row); 266 } 267 } 268 }); 269 mRowsFragment.setExpand(true); 270 if (null != mProvider) { 271 onSetSearchResultProvider(); 272 } 273 updateSearchBar(); 274 return root; 275 } 276 277 @Override 278 public void onStart() { 279 super.onStart(); 280 281 VerticalGridView list = mRowsFragment.getVerticalGridView(); 282 int mContainerListAlignTop = 283 getResources().getDimensionPixelSize(R.dimen.lb_search_browse_rows_align_top); 284 list.setItemAlignmentOffset(0); 285 list.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 286 list.setWindowAlignmentOffset(mContainerListAlignTop); 287 list.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 288 list.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 289 } 290 291 @Override 292 public void onResume() { 293 super.onResume(); 294 if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) { 295 mHandler.removeMessages(MSG_DESTROY_RECOGNIZER); 296 mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity()); 297 mSearchBar.setSpeechRecognizer(mSpeechRecognizer); 298 } 299 } 300 301 @Override 302 public void onPause() { 303 releaseRecognizer(); 304 super.onPause(); 305 } 306 307 @Override 308 public void onDestroy() { 309 releaseAdapter(); 310 super.onDestroy(); 311 } 312 313 private void releaseRecognizer() { 314 if (null != mSpeechRecognizer) { 315 mSearchBar.setSpeechRecognizer(null); 316 mSpeechRecognizer.stopListening(); 317 mSpeechRecognizer.cancel(); 318 mHandler.sendEmptyMessageDelayed(MSG_DESTROY_RECOGNIZER, 1000); 319 } 320 } 321 322 /** 323 * Set the search provider that is responsible for returning results for the 324 * search query. 325 */ 326 public void setSearchResultProvider(SearchResultProvider searchResultProvider) { 327 mProvider = searchResultProvider; 328 onSetSearchResultProvider(); 329 } 330 331 /** 332 * Sets an item selection listener for the results. 333 * 334 * @param listener The item selection listener to be invoked when an item in 335 * the search results is selected. 336 * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)} 337 */ 338 public void setOnItemSelectedListener(OnItemSelectedListener listener) { 339 mOnItemSelectedListener = listener; 340 } 341 342 /** 343 * Sets an item clicked listener for the results. 344 * 345 * @param listener The item clicked listener to be invoked when an item in 346 * the search results is clicked. 347 * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)} 348 */ 349 public void setOnItemClickedListener(OnItemClickedListener listener) { 350 mOnItemClickedListener = listener; 351 } 352 353 /** 354 * Sets an item selection listener for the results. 355 * 356 * @param listener The item selection listener to be invoked when an item in 357 * the search results is selected. 358 */ 359 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 360 mOnItemViewSelectedListener = listener; 361 } 362 363 /** 364 * Sets an item clicked listener for the results. 365 * 366 * @param listener The item clicked listener to be invoked when an item in 367 * the search results is clicked. 368 */ 369 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 370 mOnItemViewClickedListener = listener; 371 } 372 373 /** 374 * Sets the title string to be be shown in an empty search bar. The title 375 * may be placed in a call-to-action, such as "Search <i>title</i>" or 376 * "Speak to search <i>title</i>". 377 */ 378 public void setTitle(String title) { 379 mTitle = title; 380 if (null != mSearchBar) { 381 mSearchBar.setTitle(title); 382 } 383 } 384 385 /** 386 * Returns the title set in the search bar. 387 */ 388 public String getTitle() { 389 if (null != mSearchBar) { 390 return mSearchBar.getTitle(); 391 } 392 return null; 393 } 394 395 /** 396 * Sets the badge drawable that will be shown inside the search bar next to 397 * the title. 398 */ 399 public void setBadgeDrawable(Drawable drawable) { 400 mBadgeDrawable = drawable; 401 if (null != mSearchBar) { 402 mSearchBar.setBadgeDrawable(drawable); 403 } 404 } 405 406 /** 407 * Returns the badge drawable in the search bar. 408 */ 409 public Drawable getBadgeDrawable() { 410 if (null != mSearchBar) { 411 return mSearchBar.getBadgeDrawable(); 412 } 413 return null; 414 } 415 416 /** 417 * Display the completions shown by the IME. An application may provide 418 * a list of query completions that the system will show in the IME. 419 * 420 * @param completions A list of completions to show in the IME. Setting to 421 * null or empty will clear the list. 422 */ 423 public void displayCompletions(List<String> completions) { 424 mSearchBar.displayCompletions(completions); 425 } 426 427 /** 428 * Set this callback to have the fragment pass speech recognition requests 429 * to the activity rather than using an internal recognizer. 430 */ 431 public void setSpeechRecognitionCallback(SpeechRecognitionCallback callback) { 432 mSpeechRecognitionCallback = callback; 433 if (mSearchBar != null) { 434 mSearchBar.setSpeechRecognitionCallback(mSpeechRecognitionCallback); 435 } 436 if (callback != null) { 437 releaseRecognizer(); 438 } 439 } 440 441 /** 442 * Sets the text of the search query and optionally submits the query. Either 443 * {@link SearchResultProvider#onQueryTextChange onQueryTextChange} or 444 * {@link SearchResultProvider#onQueryTextSubmit onQueryTextSubmit} will be 445 * called on the provider if it is set. 446 * 447 * @param query The search query to set. 448 * @param submit Whether to submit the query. 449 */ 450 public void setSearchQuery(String query, boolean submit) { 451 // setSearchQuery will call onQueryTextChange 452 mSearchBar.setSearchQuery(query); 453 if (submit) { 454 mProvider.onQueryTextSubmit(query); 455 } 456 } 457 458 /** 459 * Sets the text of the search query based on the {@link RecognizerIntent#EXTRA_RESULTS} in 460 * the given intent, and optionally submit the query. If more than one result is present 461 * in the results list, the first will be used. 462 * 463 * @param intent Intent received from a speech recognition service. 464 * @param submit Whether to submit the query. 465 */ 466 public void setSearchQuery(Intent intent, boolean submit) { 467 ArrayList<String> matches = intent.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); 468 if (matches != null && matches.size() > 0) { 469 setSearchQuery(matches.get(0), submit); 470 } 471 } 472 473 /** 474 * Returns an intent that can be used to request speech recognition. 475 * Built from the base {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH} plus 476 * extras: 477 * 478 * <ul> 479 * <li>{@link RecognizerIntent#EXTRA_LANGUAGE_MODEL} set to 480 * {@link RecognizerIntent#LANGUAGE_MODEL_FREE_FORM}</li> 481 * <li>{@link RecognizerIntent#EXTRA_PARTIAL_RESULTS} set to true</li> 482 * <li>{@link RecognizerIntent#EXTRA_PROMPT} set to the search bar hint text</li> 483 * </ul> 484 * 485 * For handling the intent returned from the service, see 486 * {@link #setSearchQuery(Intent, boolean)}. 487 */ 488 public Intent getRecognizerIntent() { 489 Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 490 recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 491 RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); 492 recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); 493 if (mSearchBar != null && mSearchBar.getHint() != null) { 494 recognizerIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, mSearchBar.getHint()); 495 } 496 return recognizerIntent; 497 } 498 499 private void retrieveResults(String searchQuery) { 500 if (DEBUG) Log.v(TAG, String.format("retrieveResults %s", searchQuery)); 501 mProvider.onQueryTextChange(searchQuery); 502 mStatus &= ~QUERY_COMPLETE; 503 } 504 505 private void queryComplete() { 506 mStatus |= QUERY_COMPLETE; 507 focusOnResults(); 508 } 509 510 private void resultsChanged() { 511 if (DEBUG) Log.v(TAG, "adapter size " + mResultAdapter.size()); 512 mStatus |= RESULTS_CHANGED; 513 if ((mStatus & QUERY_COMPLETE) != 0) { 514 focusOnResults(); 515 } 516 updateSearchBar(); 517 } 518 519 private void updateSearchBar() { 520 if (mSearchBar == null || mResultAdapter == null) { 521 return; 522 } 523 final int viewId = (mResultAdapter.size() == 0 || mRowsFragment == null || 524 mRowsFragment.getVerticalGridView() == null) ? 0 : 525 mRowsFragment.getVerticalGridView().getId(); 526 mSearchBar.setNextFocusDownId(viewId); 527 } 528 529 private void focusOnResults() { 530 if (mRowsFragment == null || 531 mRowsFragment.getVerticalGridView() == null || 532 mResultAdapter.size() == 0) { 533 return; 534 } 535 mRowsFragment.setSelectedPosition(0); 536 if (mRowsFragment.getVerticalGridView().requestFocus()) { 537 mStatus &= ~RESULTS_CHANGED; 538 } 539 } 540 541 private void onSetSearchResultProvider() { 542 mHandler.post(new Runnable() { 543 @Override 544 public void run() { 545 // Retrieve the result adapter 546 ObjectAdapter adapter = mProvider.getResultsAdapter(); 547 if (adapter != mResultAdapter) { 548 releaseAdapter(); 549 mResultAdapter = adapter; 550 if (mResultAdapter != null) { 551 mResultAdapter.registerObserver(mAdapterObserver); 552 } 553 } 554 if (null != mRowsFragment) { 555 mRowsFragment.setAdapter(mResultAdapter); 556 executePendingQuery(); 557 } 558 updateSearchBar(); 559 } 560 }); 561 } 562 563 private void releaseAdapter() { 564 if (mResultAdapter != null) { 565 mResultAdapter.unregisterObserver(mAdapterObserver); 566 mResultAdapter = null; 567 } 568 } 569 570 private void executePendingQuery() { 571 if (null != mPendingQuery && null != mResultAdapter) { 572 String query = mPendingQuery; 573 mPendingQuery = null; 574 retrieveResults(query); 575 } 576 } 577 578 private void readArguments(Bundle args) { 579 if (null == args) { 580 return; 581 } 582 if (args.containsKey(ARG_QUERY)) { 583 setSearchQuery(args.getString(ARG_QUERY)); 584 } 585 586 if (args.containsKey(ARG_TITLE)) { 587 setTitle(args.getString(ARG_TITLE)); 588 } 589 } 590 591 private void setSearchQuery(String query) { 592 mSearchBar.setSearchQuery(query); 593 } 594} 595