2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.dashboard;
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;
46 import java.util.HashMap;
48 public class SearchResultsSummary extends InstrumentedFragment {
50 private static final String LOG_TAG = "SearchResultsSummary";
52 private static final String EMPTY_QUERY = "";
53 private static char ELLIPSIS = '\u2026';
55 private static final String SAVE_KEY_SHOW_RESULTS = ":settings:show_results";
57 private SearchView mSearchView;
59 private ListView mResultsListView;
60 private SearchResultsAdapter mResultsAdapter;
61 private UpdateSearchResultsTask mUpdateSearchResultsTask;
63 private ListView mSuggestionsListView;
64 private SuggestionsAdapter mSuggestionsAdapter;
65 private UpdateSuggestionsTask mUpdateSuggestionsTask;
67 private ViewGroup mLayoutSuggestions;
68 private ViewGroup mLayoutResults;
70 private String mQuery;
72 private boolean mShowResults;
75 * A basic AsyncTask for updating the query results cursor
77 private class UpdateSearchResultsTask extends AsyncTask<String, Void, Cursor> {
79 protected Cursor doInBackground(String... params) {
80 return Index.getInstance(getActivity()).search(params[0]);
84 protected void onPostExecute(Cursor cursor) {
86 MetricsLogger.action(getContext(), MetricsLogger.ACTION_SEARCH_RESULTS,
88 setResultsCursor(cursor);
89 setResultsVisibility(cursor.getCount() > 0);
90 } else if (cursor != null) {
97 * A basic AsyncTask for updating the suggestions cursor
99 private class UpdateSuggestionsTask extends AsyncTask<String, Void, Cursor> {
101 protected Cursor doInBackground(String... params) {
102 return Index.getInstance(getActivity()).getSuggestions(params[0]);
106 protected void onPostExecute(Cursor cursor) {
107 if (!isCancelled()) {
108 setSuggestionsCursor(cursor);
109 setSuggestionsVisibility(cursor.getCount() > 0);
110 } else if (cursor != null) {
117 public void onCreate(Bundle savedInstanceState) {
118 super.onCreate(savedInstanceState);
120 mResultsAdapter = new SearchResultsAdapter(getActivity());
121 mSuggestionsAdapter = new SuggestionsAdapter(getActivity());
123 if (savedInstanceState != null) {
124 mShowResults = savedInstanceState.getBoolean(SAVE_KEY_SHOW_RESULTS);
129 public void onSaveInstanceState(Bundle outState) {
130 super.onSaveInstanceState(outState);
132 outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults);
136 public void onStop() {
144 public void onDestroy() {
145 mResultsListView = null;
146 mResultsAdapter = null;
147 mUpdateSearchResultsTask = null;
149 mSuggestionsListView = null;
150 mSuggestionsAdapter = null;
151 mUpdateSuggestionsTask = null;
159 public View onCreateView(LayoutInflater inflater, ViewGroup container,
160 Bundle savedInstanceState) {
162 final View view = inflater.inflate(R.layout.search_panel, container, false);
164 mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions);
165 mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results);
167 mResultsListView = (ListView) view.findViewById(R.id.list_results);
168 mResultsListView.setAdapter(mResultsAdapter);
169 mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
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
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"
181 final Cursor cursor = mResultsAdapter.mCursor;
182 cursor.moveToPosition(position);
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);
189 final SettingsActivity sa = (SettingsActivity) getActivity();
190 sa.needToRevertToInitialFragment();
192 if (TextUtils.isEmpty(action)) {
193 Bundle args = new Bundle();
194 args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
196 Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle);
198 final Intent intent = new Intent(action);
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);
209 intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
211 sa.startActivity(intent);
214 saveQueryToDatabase();
217 mResultsListView.addHeaderView(
218 LayoutInflater.from(getActivity()).inflate(
219 R.layout.search_panel_results_header, mResultsListView, false),
222 mSuggestionsListView = (ListView) view.findViewById(R.id.list_suggestions);
223 mSuggestionsListView.setAdapter(mSuggestionsAdapter);
224 mSuggestionsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
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
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"
234 final Cursor cursor = mSuggestionsAdapter.mCursor;
235 cursor.moveToPosition(position);
238 mQuery = cursor.getString(0);
239 if (mSearchView != null) {
240 mSearchView.setQuery(mQuery, false);
244 mSuggestionsListView.addHeaderView(
245 LayoutInflater.from(getActivity()).inflate(
246 R.layout.search_panel_suggestions_header, mSuggestionsListView, false),
253 protected int getMetricsCategory() {
254 return MetricsLogger.DASHBOARD_SEARCH_RESULTS;
258 public void onResume() {
262 showSomeSuggestions();
266 public void setSearchView(SearchView searchView) {
267 mSearchView = searchView;
270 private void setSuggestionsVisibility(boolean visible) {
271 if (mLayoutSuggestions != null) {
272 mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE);
276 private void setResultsVisibility(boolean visible) {
277 if (mLayoutResults != null) {
278 mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE);
282 private void saveQueryToDatabase() {
283 Index.getInstance(getActivity()).addSavedQuery(mQuery);
286 public boolean onQueryTextSubmit(String query) {
287 mQuery = getFilteredQueryString(query);
289 setSuggestionsVisibility(false);
290 updateSearchResults();
291 saveQueryToDatabase();
296 public boolean onQueryTextChange(String query) {
297 final String newQuery = getFilteredQueryString(query);
301 if (TextUtils.isEmpty(mQuery)) {
302 mShowResults = false;
303 setResultsVisibility(false);
307 setSuggestionsVisibility(false);
308 updateSearchResults();
314 public void showSomeSuggestions() {
315 setResultsVisibility(false);
316 mQuery = EMPTY_QUERY;
320 private void clearSuggestions() {
321 if (mUpdateSuggestionsTask != null) {
322 mUpdateSuggestionsTask.cancel(false);
323 mUpdateSuggestionsTask = null;
325 setSuggestionsCursor(null);
328 private void setSuggestionsCursor(Cursor cursor) {
329 if (mSuggestionsAdapter == null) {
332 Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor);
333 if (oldCursor != null) {
338 private void clearResults() {
339 if (mUpdateSearchResultsTask != null) {
340 mUpdateSearchResultsTask.cancel(false);
341 mUpdateSearchResultsTask = null;
343 setResultsCursor(null);
346 private void setResultsCursor(Cursor cursor) {
347 if (mResultsAdapter == null) {
350 Cursor oldCursor = mResultsAdapter.swapCursor(cursor);
351 if (oldCursor != null) {
356 private String getFilteredQueryString(CharSequence query) {
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)) {
368 return filtered.toString();
371 private void clearAllTasks() {
372 if (mUpdateSearchResultsTask != null) {
373 mUpdateSearchResultsTask.cancel(false);
374 mUpdateSearchResultsTask = null;
376 if (mUpdateSuggestionsTask != null) {
377 mUpdateSuggestionsTask.cancel(false);
378 mUpdateSuggestionsTask = null;
382 private void updateSuggestions() {
384 if (mQuery == null) {
385 setSuggestionsCursor(null);
387 mUpdateSuggestionsTask = new UpdateSuggestionsTask();
388 mUpdateSuggestionsTask.execute(mQuery);
392 private void updateSearchResults() {
394 if (TextUtils.isEmpty(mQuery)) {
395 setResultsVisibility(false);
396 setResultsCursor(null);
398 mUpdateSearchResultsTask = new UpdateSearchResultsTask();
399 mUpdateSearchResultsTask.execute(mQuery);
403 private static class SuggestionItem {
406 public SuggestionItem(String query) {
411 private static class SuggestionsAdapter extends BaseAdapter {
413 private static final int COLUMN_SUGGESTION_QUERY = 0;
414 private static final int COLUMN_SUGGESTION_TIMESTAMP = 1;
416 private Context mContext;
417 private Cursor mCursor;
418 private LayoutInflater mInflater;
419 private boolean mDataValid = false;
421 public SuggestionsAdapter(Context context) {
423 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
427 public Cursor swapCursor(Cursor newCursor) {
428 if (newCursor == mCursor) {
431 Cursor oldCursor = mCursor;
433 if (newCursor != null) {
435 notifyDataSetChanged();
438 notifyDataSetInvalidated();
444 public int getCount() {
445 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
446 return mCursor.getCount();
450 public Object getItem(int position) {
451 if (mDataValid && mCursor.moveToPosition(position)) {
452 final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY);
454 return new SuggestionItem(query);
460 public long getItemId(int position) {
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");
470 if (!mCursor.moveToPosition(position)) {
471 throw new IllegalStateException("couldn't move cursor to position " + position);
476 if (convertView == null) {
477 view = mInflater.inflate(R.layout.search_suggestion_item, parent, false);
482 TextView query = (TextView) view.findViewById(R.id.title);
484 SuggestionItem item = (SuggestionItem) getItem(position);
485 query.setText(item.query);
491 private static class SearchResult {
492 public Context context;
494 public String summaryOn;
495 public String summaryOff;
496 public String entries;
497 public int iconResId;
500 public SearchResult(Context context, String title, String summaryOn, String summaryOff,
501 String entries, int iconResId, String key) {
502 this.context = context;
504 this.summaryOn = summaryOn;
505 this.summaryOff = summaryOff;
506 this.entries = entries;
507 this.iconResId = iconResId;
512 private static class SearchResultsAdapter extends BaseAdapter {
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>();
520 private static final String PERCENT_RECLACE = "%s";
521 private static final String DOLLAR_REPLACE = "$s";
523 public SearchResultsAdapter(Context context) {
525 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
529 public Cursor swapCursor(Cursor newCursor) {
530 if (newCursor == mCursor) {
533 Cursor oldCursor = mCursor;
535 if (newCursor != null) {
537 notifyDataSetChanged();
540 notifyDataSetInvalidated();
546 public int getCount() {
547 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
548 return mCursor.getCount();
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);
566 Context packageContext;
567 if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) {
568 packageContext = mContextMap.get(packageName);
569 if (packageContext == null) {
571 packageContext = mContext.createPackageContext(packageName, 0);
572 } catch (PackageManager.NameNotFoundException e) {
573 Log.e(LOG_TAG, "Cannot create Context for package: " + packageName);
576 mContextMap.put(packageName, packageContext);
579 packageContext = mContext;
582 final int iconResId = TextUtils.isEmpty(iconResStr) ?
583 R.drawable.empty_icon : Integer.parseInt(iconResStr);
585 return new SearchResult(packageContext, title, summaryOn, summaryOff,
586 entries, iconResId, key);
592 public long getItemId(int position) {
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");
602 if (!mCursor.moveToPosition(position)) {
603 throw new IllegalStateException("couldn't move cursor to position " + position);
610 if (convertView == null) {
611 view = mInflater.inflate(R.layout.search_result_item, parent, false);
616 textTitle = (TextView) view.findViewById(R.id.title);
617 imageView = (ImageView) view.findViewById(R.id.icon);
619 final SearchResult result = (SearchResult) getItem(position);
620 textTitle.setText(result.title);
622 if (result.iconResId != R.drawable.empty_icon) {
623 final Context packageContext = result.context;
624 final Drawable drawable;
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);
633 imageView.setImageDrawable(null);
634 imageView.setBackgroundResource(R.drawable.empty_icon);