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