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 doInBackground(String... params)78 protected Cursor doInBackground(String... params) { 79 return Index.getInstance(getActivity()).search(params[0]); 80 } 81 82 @Override onPostExecute(Cursor cursor)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 doInBackground(String... params)98 protected Cursor doInBackground(String... params) { 99 return Index.getInstance(getActivity()).getSuggestions(params[0]); 100 } 101 102 @Override onPostExecute(Cursor cursor)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 onCreate(Bundle savedInstanceState)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 onSaveInstanceState(Bundle outState)126 public void onSaveInstanceState(Bundle outState) { 127 super.onSaveInstanceState(outState); 128 129 outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults); 130 } 131 132 @Override onStop()133 public void onStop() { 134 super.onStop(); 135 136 clearSuggestions(); 137 clearResults(); 138 } 139 140 @Override onDestroy()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 onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 onResume()248 public void onResume() { 249 super.onResume(); 250 251 if (!mShowResults) { 252 showSomeSuggestions(); 253 } 254 } 255 setSearchView(SearchView searchView)256 public void setSearchView(SearchView searchView) { 257 mSearchView = searchView; 258 } 259 setSuggestionsVisibility(boolean visible)260 private void setSuggestionsVisibility(boolean visible) { 261 if (mLayoutSuggestions != null) { 262 mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE); 263 } 264 } 265 setResultsVisibility(boolean visible)266 private void setResultsVisibility(boolean visible) { 267 if (mLayoutResults != null) { 268 mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE); 269 } 270 } 271 saveQueryToDatabase()272 private void saveQueryToDatabase() { 273 Index.getInstance(getActivity()).addSavedQuery(mQuery); 274 } 275 onQueryTextSubmit(String query)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 onQueryTextChange(String query)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 showSomeSuggestions()304 public void showSomeSuggestions() { 305 setResultsVisibility(false); 306 mQuery = EMPTY_QUERY; 307 updateSuggestions(); 308 } 309 clearSuggestions()310 private void clearSuggestions() { 311 if (mUpdateSuggestionsTask != null) { 312 mUpdateSuggestionsTask.cancel(false); 313 mUpdateSuggestionsTask = null; 314 } 315 setSuggestionsCursor(null); 316 } 317 setSuggestionsCursor(Cursor cursor)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 clearResults()328 private void clearResults() { 329 if (mUpdateSearchResultsTask != null) { 330 mUpdateSearchResultsTask.cancel(false); 331 mUpdateSearchResultsTask = null; 332 } 333 setResultsCursor(null); 334 } 335 setResultsCursor(Cursor cursor)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 getFilteredQueryString(CharSequence query)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 clearAllTasks()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 updateSuggestions()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 updateSearchResults()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 SuggestionItem(String query)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 SuggestionsAdapter(Context context)411 public SuggestionsAdapter(Context context) { 412 mContext = context; 413 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 414 mDataValid = false; 415 } 416 swapCursor(Cursor newCursor)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 getCount()434 public int getCount() { 435 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; 436 return mCursor.getCount(); 437 } 438 439 @Override getItem(int position)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 getItemId(int position)450 public long getItemId(int position) { 451 return 0; 452 } 453 454 @Override getView(int position, View convertView, ViewGroup parent)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 SearchResult(Context context, String title, String summaryOn, String summaryOff, String entries, int iconResId, String key)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 SearchResultsAdapter(Context context)513 public SearchResultsAdapter(Context context) { 514 mContext = context; 515 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 516 mDataValid = false; 517 } 518 swapCursor(Cursor newCursor)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 getCount()536 public int getCount() { 537 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; 538 return mCursor.getCount(); 539 } 540 541 @Override getItem(int position)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 getItemId(int position)582 public long getItemId(int position) { 583 return 0; 584 } 585 586 @Override getView(int position, View convertView, ViewGroup parent)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