1/* 2 * Copyright (C) 2014 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.settings.dashboard; 18 19import android.app.Fragment; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.content.pm.PackageManager; 24import android.content.res.Resources; 25import android.database.Cursor; 26import android.graphics.drawable.Drawable; 27import android.os.AsyncTask; 28import android.os.Bundle; 29import android.text.TextUtils; 30import android.util.Log; 31import android.view.LayoutInflater; 32import android.view.View; 33import android.view.ViewGroup; 34import android.widget.AdapterView; 35import android.widget.BaseAdapter; 36import android.widget.ImageView; 37import android.widget.ListView; 38import android.widget.SearchView; 39import android.widget.TextView; 40import com.android.settings.R; 41import com.android.settings.SettingsActivity; 42import com.android.settings.Utils; 43import com.android.settings.search.Index; 44 45import java.util.HashMap; 46 47public class SearchResultsSummary extends Fragment { 48 49 private static final String LOG_TAG = "SearchResultsSummary"; 50 51 private static final String EMPTY_QUERY = ""; 52 private static char ELLIPSIS = '\u2026'; 53 54 private static final String SAVE_KEY_SHOW_RESULTS = ":settings:show_results"; 55 56 private SearchView mSearchView; 57 58 private ListView mResultsListView; 59 private SearchResultsAdapter mResultsAdapter; 60 private UpdateSearchResultsTask mUpdateSearchResultsTask; 61 62 private ListView mSuggestionsListView; 63 private SuggestionsAdapter mSuggestionsAdapter; 64 private UpdateSuggestionsTask mUpdateSuggestionsTask; 65 66 private ViewGroup mLayoutSuggestions; 67 private ViewGroup mLayoutResults; 68 69 private String mQuery; 70 71 private boolean mShowResults; 72 73 /** 74 * A basic AsyncTask for updating the query results cursor 75 */ 76 private class UpdateSearchResultsTask extends AsyncTask<String, Void, Cursor> { 77 @Override 78 protected Cursor doInBackground(String... params) { 79 return Index.getInstance(getActivity()).search(params[0]); 80 } 81 82 @Override 83 protected void onPostExecute(Cursor cursor) { 84 if (!isCancelled()) { 85 setResultsCursor(cursor); 86 setResultsVisibility(cursor.getCount() > 0); 87 } else if (cursor != null) { 88 cursor.close(); 89 } 90 } 91 } 92 93 /** 94 * A basic AsyncTask for updating the suggestions cursor 95 */ 96 private class UpdateSuggestionsTask extends AsyncTask<String, Void, Cursor> { 97 @Override 98 protected Cursor doInBackground(String... params) { 99 return Index.getInstance(getActivity()).getSuggestions(params[0]); 100 } 101 102 @Override 103 protected void onPostExecute(Cursor cursor) { 104 if (!isCancelled()) { 105 setSuggestionsCursor(cursor); 106 setSuggestionsVisibility(cursor.getCount() > 0); 107 } else if (cursor != null) { 108 cursor.close(); 109 } 110 } 111 } 112 113 @Override 114 public void onCreate(Bundle savedInstanceState) { 115 super.onCreate(savedInstanceState); 116 117 mResultsAdapter = new SearchResultsAdapter(getActivity()); 118 mSuggestionsAdapter = new SuggestionsAdapter(getActivity()); 119 120 if (savedInstanceState != null) { 121 mShowResults = savedInstanceState.getBoolean(SAVE_KEY_SHOW_RESULTS); 122 } 123 } 124 125 @Override 126 public void onSaveInstanceState(Bundle outState) { 127 super.onSaveInstanceState(outState); 128 129 outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults); 130 } 131 132 @Override 133 public void onStop() { 134 super.onStop(); 135 136 clearSuggestions(); 137 clearResults(); 138 } 139 140 @Override 141 public void onDestroy() { 142 mResultsListView = null; 143 mResultsAdapter = null; 144 mUpdateSearchResultsTask = null; 145 146 mSuggestionsListView = null; 147 mSuggestionsAdapter = null; 148 mUpdateSuggestionsTask = null; 149 150 mSearchView = null; 151 152 super.onDestroy(); 153 } 154 155 @Override 156 public View onCreateView(LayoutInflater inflater, ViewGroup container, 157 Bundle savedInstanceState) { 158 159 final View view = inflater.inflate(R.layout.search_panel, container, false); 160 161 mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions); 162 mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results); 163 164 mResultsListView = (ListView) view.findViewById(R.id.list_results); 165 mResultsListView.setAdapter(mResultsAdapter); 166 mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 167 @Override 168 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 169 // We have a header, so we need to decrement the position by one 170 position--; 171 172 // Some Monkeys could create a case where they were probably clicking on the 173 // List Header and thus the position passed was "0" and then by decrement was "-1" 174 if (position < 0) { 175 return; 176 } 177 178 final Cursor cursor = mResultsAdapter.mCursor; 179 cursor.moveToPosition(position); 180 181 final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME); 182 final String screenTitle = cursor.getString(Index.COLUMN_INDEX_SCREEN_TITLE); 183 final String action = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION); 184 final String key = cursor.getString(Index.COLUMN_INDEX_KEY); 185 186 final SettingsActivity sa = (SettingsActivity) getActivity(); 187 sa.needToRevertToInitialFragment(); 188 189 if (TextUtils.isEmpty(action)) { 190 Bundle args = new Bundle(); 191 args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); 192 193 Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle); 194 } else { 195 final Intent intent = new Intent(action); 196 197 final String targetPackage = cursor.getString( 198 Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); 199 final String targetClass = cursor.getString( 200 Index.COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS); 201 if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) { 202 final ComponentName component = 203 new ComponentName(targetPackage, targetClass); 204 intent.setComponent(component); 205 } 206 intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); 207 208 sa.startActivity(intent); 209 } 210 211 saveQueryToDatabase(); 212 } 213 }); 214 mResultsListView.addHeaderView( 215 LayoutInflater.from(getActivity()).inflate( 216 R.layout.search_panel_results_header, mResultsListView, false), 217 null, false); 218 219 mSuggestionsListView = (ListView) view.findViewById(R.id.list_suggestions); 220 mSuggestionsListView.setAdapter(mSuggestionsAdapter); 221 mSuggestionsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 222 @Override 223 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 224 // We have a header, so we need to decrement the position by one 225 position--; 226 // Some Monkeys could create a case where they were probably clicking on the 227 // List Header and thus the position passed was "0" and then by decrement was "-1" 228 if (position < 0) { 229 return; 230 } 231 final Cursor cursor = mSuggestionsAdapter.mCursor; 232 cursor.moveToPosition(position); 233 234 mShowResults = true; 235 mQuery = cursor.getString(0); 236 mSearchView.setQuery(mQuery, false); 237 } 238 }); 239 mSuggestionsListView.addHeaderView( 240 LayoutInflater.from(getActivity()).inflate( 241 R.layout.search_panel_suggestions_header, mSuggestionsListView, false), 242 null, false); 243 244 return view; 245 } 246 247 @Override 248 public void onResume() { 249 super.onResume(); 250 251 if (!mShowResults) { 252 showSomeSuggestions(); 253 } 254 } 255 256 public void setSearchView(SearchView searchView) { 257 mSearchView = searchView; 258 } 259 260 private void setSuggestionsVisibility(boolean visible) { 261 if (mLayoutSuggestions != null) { 262 mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE); 263 } 264 } 265 266 private void setResultsVisibility(boolean visible) { 267 if (mLayoutResults != null) { 268 mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE); 269 } 270 } 271 272 private void saveQueryToDatabase() { 273 Index.getInstance(getActivity()).addSavedQuery(mQuery); 274 } 275 276 public boolean onQueryTextSubmit(String query) { 277 mQuery = getFilteredQueryString(query); 278 mShowResults = true; 279 setSuggestionsVisibility(false); 280 updateSearchResults(); 281 saveQueryToDatabase(); 282 return true; 283 } 284 285 public boolean onQueryTextChange(String query) { 286 final String newQuery = getFilteredQueryString(query); 287 288 mQuery = newQuery; 289 290 if (TextUtils.isEmpty(mQuery)) { 291 mShowResults = false; 292 setResultsVisibility(false); 293 updateSuggestions(); 294 } else { 295 mShowResults = true; 296 setSuggestionsVisibility(false); 297 updateSearchResults(); 298 } 299 300 return true; 301 } 302 303 public void showSomeSuggestions() { 304 setResultsVisibility(false); 305 mQuery = EMPTY_QUERY; 306 updateSuggestions(); 307 } 308 309 private void clearSuggestions() { 310 if (mUpdateSuggestionsTask != null) { 311 mUpdateSuggestionsTask.cancel(false); 312 mUpdateSuggestionsTask = null; 313 } 314 setSuggestionsCursor(null); 315 } 316 317 private void setSuggestionsCursor(Cursor cursor) { 318 if (mSuggestionsAdapter == null) { 319 return; 320 } 321 Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor); 322 if (oldCursor != null) { 323 oldCursor.close(); 324 } 325 } 326 327 private void clearResults() { 328 if (mUpdateSearchResultsTask != null) { 329 mUpdateSearchResultsTask.cancel(false); 330 mUpdateSearchResultsTask = null; 331 } 332 setResultsCursor(null); 333 } 334 335 private void setResultsCursor(Cursor cursor) { 336 if (mResultsAdapter == null) { 337 return; 338 } 339 Cursor oldCursor = mResultsAdapter.swapCursor(cursor); 340 if (oldCursor != null) { 341 oldCursor.close(); 342 } 343 } 344 345 private String getFilteredQueryString(CharSequence query) { 346 if (query == null) { 347 return null; 348 } 349 final StringBuilder filtered = new StringBuilder(); 350 for (int n = 0; n < query.length(); n++) { 351 char c = query.charAt(n); 352 if (!Character.isLetterOrDigit(c) && !Character.isSpaceChar(c)) { 353 continue; 354 } 355 filtered.append(c); 356 } 357 return filtered.toString(); 358 } 359 360 private void clearAllTasks() { 361 if (mUpdateSearchResultsTask != null) { 362 mUpdateSearchResultsTask.cancel(false); 363 mUpdateSearchResultsTask = null; 364 } 365 if (mUpdateSuggestionsTask != null) { 366 mUpdateSuggestionsTask.cancel(false); 367 mUpdateSuggestionsTask = null; 368 } 369 } 370 371 private void updateSuggestions() { 372 clearAllTasks(); 373 if (mQuery == null) { 374 setSuggestionsCursor(null); 375 } else { 376 mUpdateSuggestionsTask = new UpdateSuggestionsTask(); 377 mUpdateSuggestionsTask.execute(mQuery); 378 } 379 } 380 381 private void updateSearchResults() { 382 clearAllTasks(); 383 if (TextUtils.isEmpty(mQuery)) { 384 setResultsVisibility(false); 385 setResultsCursor(null); 386 } else { 387 mUpdateSearchResultsTask = new UpdateSearchResultsTask(); 388 mUpdateSearchResultsTask.execute(mQuery); 389 } 390 } 391 392 private static class SuggestionItem { 393 public String query; 394 395 public SuggestionItem(String query) { 396 this.query = query; 397 } 398 } 399 400 private static class SuggestionsAdapter extends BaseAdapter { 401 402 private static final int COLUMN_SUGGESTION_QUERY = 0; 403 private static final int COLUMN_SUGGESTION_TIMESTAMP = 1; 404 405 private Context mContext; 406 private Cursor mCursor; 407 private LayoutInflater mInflater; 408 private boolean mDataValid = false; 409 410 public SuggestionsAdapter(Context context) { 411 mContext = context; 412 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 413 mDataValid = false; 414 } 415 416 public Cursor swapCursor(Cursor newCursor) { 417 if (newCursor == mCursor) { 418 return null; 419 } 420 Cursor oldCursor = mCursor; 421 mCursor = newCursor; 422 if (newCursor != null) { 423 mDataValid = true; 424 notifyDataSetChanged(); 425 } else { 426 mDataValid = false; 427 notifyDataSetInvalidated(); 428 } 429 return oldCursor; 430 } 431 432 @Override 433 public int getCount() { 434 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; 435 return mCursor.getCount(); 436 } 437 438 @Override 439 public Object getItem(int position) { 440 if (mDataValid && mCursor.moveToPosition(position)) { 441 final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY); 442 443 return new SuggestionItem(query); 444 } 445 return null; 446 } 447 448 @Override 449 public long getItemId(int position) { 450 return 0; 451 } 452 453 @Override 454 public View getView(int position, View convertView, ViewGroup parent) { 455 if (!mDataValid && convertView == null) { 456 throw new IllegalStateException( 457 "this should only be called when the cursor is valid"); 458 } 459 if (!mCursor.moveToPosition(position)) { 460 throw new IllegalStateException("couldn't move cursor to position " + position); 461 } 462 463 View view; 464 465 if (convertView == null) { 466 view = mInflater.inflate(R.layout.search_suggestion_item, parent, false); 467 } else { 468 view = convertView; 469 } 470 471 TextView query = (TextView) view.findViewById(R.id.title); 472 473 SuggestionItem item = (SuggestionItem) getItem(position); 474 query.setText(item.query); 475 476 return view; 477 } 478 } 479 480 private static class SearchResult { 481 public Context context; 482 public String title; 483 public String summaryOn; 484 public String summaryOff; 485 public String entries; 486 public int iconResId; 487 public String key; 488 489 public SearchResult(Context context, String title, String summaryOn, String summaryOff, 490 String entries, int iconResId, String key) { 491 this.context = context; 492 this.title = title; 493 this.summaryOn = summaryOn; 494 this.summaryOff = summaryOff; 495 this.entries = entries; 496 this.iconResId = iconResId; 497 this.key = key; 498 } 499 } 500 501 private static class SearchResultsAdapter extends BaseAdapter { 502 503 private Context mContext; 504 private Cursor mCursor; 505 private LayoutInflater mInflater; 506 private boolean mDataValid; 507 private HashMap<String, Context> mContextMap = new HashMap<String, Context>(); 508 509 private static final String PERCENT_RECLACE = "%s"; 510 private static final String DOLLAR_REPLACE = "$s"; 511 512 public SearchResultsAdapter(Context context) { 513 mContext = context; 514 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 515 mDataValid = false; 516 } 517 518 public Cursor swapCursor(Cursor newCursor) { 519 if (newCursor == mCursor) { 520 return null; 521 } 522 Cursor oldCursor = mCursor; 523 mCursor = newCursor; 524 if (newCursor != null) { 525 mDataValid = true; 526 notifyDataSetChanged(); 527 } else { 528 mDataValid = false; 529 notifyDataSetInvalidated(); 530 } 531 return oldCursor; 532 } 533 534 @Override 535 public int getCount() { 536 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; 537 return mCursor.getCount(); 538 } 539 540 @Override 541 public Object getItem(int position) { 542 if (mDataValid && mCursor.moveToPosition(position)) { 543 final String title = mCursor.getString(Index.COLUMN_INDEX_TITLE); 544 final String summaryOn = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_ON); 545 final String summaryOff = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_OFF); 546 final String entries = mCursor.getString(Index.COLUMN_INDEX_ENTRIES); 547 final String iconResStr = mCursor.getString(Index.COLUMN_INDEX_ICON); 548 final String className = mCursor.getString( 549 Index.COLUMN_INDEX_CLASS_NAME); 550 final String packageName = mCursor.getString( 551 Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); 552 final String key = mCursor.getString( 553 Index.COLUMN_INDEX_KEY); 554 555 Context packageContext; 556 if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) { 557 packageContext = mContextMap.get(packageName); 558 if (packageContext == null) { 559 try { 560 packageContext = mContext.createPackageContext(packageName, 0); 561 } catch (PackageManager.NameNotFoundException e) { 562 Log.e(LOG_TAG, "Cannot create Context for package: " + packageName); 563 return null; 564 } 565 mContextMap.put(packageName, packageContext); 566 } 567 } else { 568 packageContext = mContext; 569 } 570 571 final int iconResId = TextUtils.isEmpty(iconResStr) ? 572 R.drawable.empty_icon : Integer.parseInt(iconResStr); 573 574 return new SearchResult(packageContext, title, summaryOn, summaryOff, 575 entries, iconResId, key); 576 } 577 return null; 578 } 579 580 @Override 581 public long getItemId(int position) { 582 return 0; 583 } 584 585 @Override 586 public View getView(int position, View convertView, ViewGroup parent) { 587 if (!mDataValid && convertView == null) { 588 throw new IllegalStateException( 589 "this should only be called when the cursor is valid"); 590 } 591 if (!mCursor.moveToPosition(position)) { 592 throw new IllegalStateException("couldn't move cursor to position " + position); 593 } 594 595 View view; 596 TextView textTitle; 597 ImageView imageView; 598 599 if (convertView == null) { 600 view = mInflater.inflate(R.layout.search_result_item, parent, false); 601 } else { 602 view = convertView; 603 } 604 605 textTitle = (TextView) view.findViewById(R.id.title); 606 imageView = (ImageView) view.findViewById(R.id.icon); 607 608 final SearchResult result = (SearchResult) getItem(position); 609 textTitle.setText(result.title); 610 611 if (result.iconResId != R.drawable.empty_icon) { 612 final Context packageContext = result.context; 613 final Drawable drawable; 614 try { 615 drawable = packageContext.getDrawable(result.iconResId); 616 imageView.setImageDrawable(drawable); 617 } catch (Resources.NotFoundException nfe) { 618 // Not much we can do except logging 619 Log.e(LOG_TAG, "Cannot load Drawable for " + result.title); 620 } 621 } else { 622 imageView.setImageDrawable(null); 623 imageView.setBackgroundResource(R.drawable.empty_icon); 624 } 625 626 return view; 627 } 628 } 629} 630