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