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