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.app.Fragment; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.graphics.drawable.Drawable; 27 import android.os.AsyncTask; 28 import android.os.Bundle; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.AdapterView; 35 import android.widget.BaseAdapter; 36 import android.widget.ImageView; 37 import android.widget.ListView; 38 import android.widget.SearchView; 39 import android.widget.TextView; 40 import com.android.settings.R; 41 import com.android.settings.SettingsActivity; 42 import com.android.settings.Utils; 43 import com.android.settings.search.Index; 44 45 import java.util.HashMap; 46 47 public 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 283 return false; 284 } 285 286 public boolean onQueryTextChange(String query) { 287 final String newQuery = getFilteredQueryString(query); 288 289 mQuery = newQuery; 290 291 if (TextUtils.isEmpty(mQuery)) { 292 mShowResults = false; 293 setResultsVisibility(false); 294 updateSuggestions(); 295 } else { 296 mShowResults = true; 297 setSuggestionsVisibility(false); 298 updateSearchResults(); 299 } 300 301 return true; 302 } 303 304 public void showSomeSuggestions() { 305 setResultsVisibility(false); 306 mQuery = EMPTY_QUERY; 307 updateSuggestions(); 308 } 309 310 private void clearSuggestions() { 311 if (mUpdateSuggestionsTask != null) { 312 mUpdateSuggestionsTask.cancel(false); 313 mUpdateSuggestionsTask = null; 314 } 315 setSuggestionsCursor(null); 316 } 317 318 private void setSuggestionsCursor(Cursor cursor) { 319 if (mSuggestionsAdapter == null) { 320 return; 321 } 322 Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor); 323 if (oldCursor != null) { 324 oldCursor.close(); 325 } 326 } 327 328 private void clearResults() { 329 if (mUpdateSearchResultsTask != null) { 330 mUpdateSearchResultsTask.cancel(false); 331 mUpdateSearchResultsTask = null; 332 } 333 setResultsCursor(null); 334 } 335 336 private void setResultsCursor(Cursor cursor) { 337 if (mResultsAdapter == null) { 338 return; 339 } 340 Cursor oldCursor = mResultsAdapter.swapCursor(cursor); 341 if (oldCursor != null) { 342 oldCursor.close(); 343 } 344 } 345 346 private String getFilteredQueryString(CharSequence query) { 347 if (query == null) { 348 return null; 349 } 350 final StringBuilder filtered = new StringBuilder(); 351 for (int n = 0; n < query.length(); n++) { 352 char c = query.charAt(n); 353 if (!Character.isLetterOrDigit(c) && !Character.isSpaceChar(c)) { 354 continue; 355 } 356 filtered.append(c); 357 } 358 return filtered.toString(); 359 } 360 361 private void clearAllTasks() { 362 if (mUpdateSearchResultsTask != null) { 363 mUpdateSearchResultsTask.cancel(false); 364 mUpdateSearchResultsTask = null; 365 } 366 if (mUpdateSuggestionsTask != null) { 367 mUpdateSuggestionsTask.cancel(false); 368 mUpdateSuggestionsTask = null; 369 } 370 } 371 372 private void updateSuggestions() { 373 clearAllTasks(); 374 if (mQuery == null) { 375 setSuggestionsCursor(null); 376 } else { 377 mUpdateSuggestionsTask = new UpdateSuggestionsTask(); 378 mUpdateSuggestionsTask.execute(mQuery); 379 } 380 } 381 382 private void updateSearchResults() { 383 clearAllTasks(); 384 if (TextUtils.isEmpty(mQuery)) { 385 setResultsVisibility(false); 386 setResultsCursor(null); 387 } else { 388 mUpdateSearchResultsTask = new UpdateSearchResultsTask(); 389 mUpdateSearchResultsTask.execute(mQuery); 390 } 391 } 392 393 private static class SuggestionItem { 394 public String query; 395 396 public SuggestionItem(String query) { 397 this.query = query; 398 } 399 } 400 401 private static class SuggestionsAdapter extends BaseAdapter { 402 403 private static final int COLUMN_SUGGESTION_QUERY = 0; 404 private static final int COLUMN_SUGGESTION_TIMESTAMP = 1; 405 406 private Context mContext; 407 private Cursor mCursor; 408 private LayoutInflater mInflater; 409 private boolean mDataValid = false; 410 411 public SuggestionsAdapter(Context context) { 412 mContext = context; 413 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 414 mDataValid = false; 415 } 416 417 public Cursor swapCursor(Cursor newCursor) { 418 if (newCursor == mCursor) { 419 return null; 420 } 421 Cursor oldCursor = mCursor; 422 mCursor = newCursor; 423 if (newCursor != null) { 424 mDataValid = true; 425 notifyDataSetChanged(); 426 } else { 427 mDataValid = false; 428 notifyDataSetInvalidated(); 429 } 430 return oldCursor; 431 } 432 433 @Override 434 public int getCount() { 435 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; 436 return mCursor.getCount(); 437 } 438 439 @Override 440 public Object getItem(int position) { 441 if (mDataValid && mCursor.moveToPosition(position)) { 442 final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY); 443 444 return new SuggestionItem(query); 445 } 446 return null; 447 } 448 449 @Override 450 public long getItemId(int position) { 451 return 0; 452 } 453 454 @Override 455 public View getView(int position, View convertView, ViewGroup parent) { 456 if (!mDataValid && convertView == null) { 457 throw new IllegalStateException( 458 "this should only be called when the cursor is valid"); 459 } 460 if (!mCursor.moveToPosition(position)) { 461 throw new IllegalStateException("couldn't move cursor to position " + position); 462 } 463 464 View view; 465 466 if (convertView == null) { 467 view = mInflater.inflate(R.layout.search_suggestion_item, parent, false); 468 } else { 469 view = convertView; 470 } 471 472 TextView query = (TextView) view.findViewById(R.id.title); 473 474 SuggestionItem item = (SuggestionItem) getItem(position); 475 query.setText(item.query); 476 477 return view; 478 } 479 } 480 481 private static class SearchResult { 482 public Context context; 483 public String title; 484 public String summaryOn; 485 public String summaryOff; 486 public String entries; 487 public int iconResId; 488 public String key; 489 490 public SearchResult(Context context, String title, String summaryOn, String summaryOff, 491 String entries, int iconResId, String key) { 492 this.context = context; 493 this.title = title; 494 this.summaryOn = summaryOn; 495 this.summaryOff = summaryOff; 496 this.entries = entries; 497 this.iconResId = iconResId; 498 this.key = key; 499 } 500 } 501 502 private static class SearchResultsAdapter extends BaseAdapter { 503 504 private Context mContext; 505 private Cursor mCursor; 506 private LayoutInflater mInflater; 507 private boolean mDataValid; 508 private HashMap<String, Context> mContextMap = new HashMap<String, Context>(); 509 510 private static final String PERCENT_RECLACE = "%s"; 511 private static final String DOLLAR_REPLACE = "$s"; 512 513 public SearchResultsAdapter(Context context) { 514 mContext = context; 515 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 516 mDataValid = false; 517 } 518 519 public Cursor swapCursor(Cursor newCursor) { 520 if (newCursor == mCursor) { 521 return null; 522 } 523 Cursor oldCursor = mCursor; 524 mCursor = newCursor; 525 if (newCursor != null) { 526 mDataValid = true; 527 notifyDataSetChanged(); 528 } else { 529 mDataValid = false; 530 notifyDataSetInvalidated(); 531 } 532 return oldCursor; 533 } 534 535 @Override 536 public int getCount() { 537 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; 538 return mCursor.getCount(); 539 } 540 541 @Override 542 public Object getItem(int position) { 543 if (mDataValid && mCursor.moveToPosition(position)) { 544 final String title = mCursor.getString(Index.COLUMN_INDEX_TITLE); 545 final String summaryOn = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_ON); 546 final String summaryOff = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_OFF); 547 final String entries = mCursor.getString(Index.COLUMN_INDEX_ENTRIES); 548 final String iconResStr = mCursor.getString(Index.COLUMN_INDEX_ICON); 549 final String className = mCursor.getString( 550 Index.COLUMN_INDEX_CLASS_NAME); 551 final String packageName = mCursor.getString( 552 Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); 553 final String key = mCursor.getString( 554 Index.COLUMN_INDEX_KEY); 555 556 Context packageContext; 557 if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) { 558 packageContext = mContextMap.get(packageName); 559 if (packageContext == null) { 560 try { 561 packageContext = mContext.createPackageContext(packageName, 0); 562 } catch (PackageManager.NameNotFoundException e) { 563 Log.e(LOG_TAG, "Cannot create Context for package: " + packageName); 564 return null; 565 } 566 mContextMap.put(packageName, packageContext); 567 } 568 } else { 569 packageContext = mContext; 570 } 571 572 final int iconResId = TextUtils.isEmpty(iconResStr) ? 573 R.drawable.empty_icon : Integer.parseInt(iconResStr); 574 575 return new SearchResult(packageContext, title, summaryOn, summaryOff, 576 entries, iconResId, key); 577 } 578 return null; 579 } 580 581 @Override 582 public long getItemId(int position) { 583 return 0; 584 } 585 586 @Override 587 public View getView(int position, View convertView, ViewGroup parent) { 588 if (!mDataValid && convertView == null) { 589 throw new IllegalStateException( 590 "this should only be called when the cursor is valid"); 591 } 592 if (!mCursor.moveToPosition(position)) { 593 throw new IllegalStateException("couldn't move cursor to position " + position); 594 } 595 596 View view; 597 TextView textTitle; 598 ImageView imageView; 599 600 if (convertView == null) { 601 view = mInflater.inflate(R.layout.search_result_item, parent, false); 602 } else { 603 view = convertView; 604 } 605 606 textTitle = (TextView) view.findViewById(R.id.title); 607 imageView = (ImageView) view.findViewById(R.id.icon); 608 609 final SearchResult result = (SearchResult) getItem(position); 610 textTitle.setText(result.title); 611 612 if (result.iconResId != R.drawable.empty_icon) { 613 final Context packageContext = result.context; 614 final Drawable drawable; 615 try { 616 drawable = packageContext.getDrawable(result.iconResId); 617 imageView.setImageDrawable(drawable); 618 } catch (Resources.NotFoundException nfe) { 619 // Not much we can do except logging 620 Log.e(LOG_TAG, "Cannot load Drawable for " + result.title); 621 } 622 } else { 623 imageView.setImageDrawable(null); 624 imageView.setBackgroundResource(R.drawable.empty_icon); 625 } 626 627 return view; 628 } 629 } 630 } 631