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 doInBackground(String... params)81 protected Cursor doInBackground(String... params) { 82 return Index.getInstance(getActivity()).search(params[0]); 83 } 84 85 @Override onPostExecute(Cursor cursor)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 doInBackground(String... params)103 protected Cursor doInBackground(String... params) { 104 return Index.getInstance(getActivity()).getSuggestions(params[0]); 105 } 106 107 @Override onPostExecute(Cursor cursor)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 onCreate(Bundle savedInstanceState)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 onSaveInstanceState(Bundle outState)131 public void onSaveInstanceState(Bundle outState) { 132 super.onSaveInstanceState(outState); 133 134 outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults); 135 } 136 137 @Override onStop()138 public void onStop() { 139 super.onStop(); 140 141 clearSuggestions(); 142 clearResults(); 143 } 144 145 @Override onDestroy()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 onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 getMetricsCategory()253 protected int getMetricsCategory() { 254 return MetricsEvent.DASHBOARD_SEARCH_RESULTS; 255 } 256 257 @Override onResume()258 public void onResume() { 259 super.onResume(); 260 261 if (!mShowResults) { 262 showSomeSuggestions(); 263 } 264 } 265 setSearchView(SearchView searchView)266 public void setSearchView(SearchView searchView) { 267 mSearchView = searchView; 268 } 269 setSuggestionsVisibility(boolean visible)270 private void setSuggestionsVisibility(boolean visible) { 271 if (mLayoutSuggestions != null) { 272 mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE); 273 } 274 } 275 setResultsVisibility(boolean visible)276 private void setResultsVisibility(boolean visible) { 277 if (mLayoutResults != null) { 278 mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE); 279 } 280 } 281 saveQueryToDatabase()282 private void saveQueryToDatabase() { 283 Index.getInstance(getActivity()).addSavedQuery(mQuery); 284 } 285 onQueryTextSubmit(String query)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 onQueryTextChange(String query)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 showSomeSuggestions()314 public void showSomeSuggestions() { 315 setResultsVisibility(false); 316 mQuery = EMPTY_QUERY; 317 updateSuggestions(); 318 } 319 clearSuggestions()320 private void clearSuggestions() { 321 if (mUpdateSuggestionsTask != null) { 322 mUpdateSuggestionsTask.cancel(false); 323 mUpdateSuggestionsTask = null; 324 } 325 setSuggestionsCursor(null); 326 } 327 setSuggestionsCursor(Cursor cursor)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 clearResults()338 private void clearResults() { 339 if (mUpdateSearchResultsTask != null) { 340 mUpdateSearchResultsTask.cancel(false); 341 mUpdateSearchResultsTask = null; 342 } 343 setResultsCursor(null); 344 } 345 setResultsCursor(Cursor cursor)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 getFilteredQueryString(CharSequence query)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 clearAllTasks()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 updateSuggestions()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 updateSearchResults()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 SuggestionItem(String query)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 SuggestionsAdapter(Context context)421 public SuggestionsAdapter(Context context) { 422 mContext = context; 423 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 424 mDataValid = false; 425 } 426 swapCursor(Cursor newCursor)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 getCount()444 public int getCount() { 445 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; 446 return mCursor.getCount(); 447 } 448 449 @Override getItem(int position)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 getItemId(int position)460 public long getItemId(int position) { 461 return 0; 462 } 463 464 @Override getView(int position, View convertView, ViewGroup parent)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 SearchResult(Context context, String title, String summaryOn, String summaryOff, String entries, int iconResId, String key)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 SearchResultsAdapter(Context context)523 public SearchResultsAdapter(Context context) { 524 mContext = context; 525 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 526 mDataValid = false; 527 } 528 swapCursor(Cursor newCursor)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 getCount()546 public int getCount() { 547 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; 548 return mCursor.getCount(); 549 } 550 551 @Override getItem(int position)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 getItemId(int position)592 public long getItemId(int position) { 593 return 0; 594 } 595 596 @Override getView(int position, View convertView, ViewGroup parent)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