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.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;
45 import java.util.HashMap;
47 public class SearchResultsSummary extends Fragment {
49 private static final String LOG_TAG = "SearchResultsSummary";
51 private static final String EMPTY_QUERY = "";
52 private static char ELLIPSIS = '\u2026';
54 private static final String SAVE_KEY_SHOW_RESULTS = ":settings:show_results";
56 private SearchView mSearchView;
58 private ListView mResultsListView;
59 private SearchResultsAdapter mResultsAdapter;
60 private UpdateSearchResultsTask mUpdateSearchResultsTask;
62 private ListView mSuggestionsListView;
63 private SuggestionsAdapter mSuggestionsAdapter;
64 private UpdateSuggestionsTask mUpdateSuggestionsTask;
66 private ViewGroup mLayoutSuggestions;
67 private ViewGroup mLayoutResults;
69 private String mQuery;
71 private boolean mShowResults;
74 * A basic AsyncTask for updating the query results cursor
76 private class UpdateSearchResultsTask extends AsyncTask<String, Void, Cursor> {
78 protected Cursor doInBackground(String... params) {
79 return Index.getInstance(getActivity()).search(params[0]);
83 protected void onPostExecute(Cursor cursor) {
85 setResultsCursor(cursor);
86 setResultsVisibility(cursor.getCount() > 0);
87 } else if (cursor != null) {
94 * A basic AsyncTask for updating the suggestions cursor
96 private class UpdateSuggestionsTask extends AsyncTask<String, Void, Cursor> {
98 protected Cursor doInBackground(String... params) {
99 return Index.getInstance(getActivity()).getSuggestions(params[0]);
103 protected void onPostExecute(Cursor cursor) {
104 if (!isCancelled()) {
105 setSuggestionsCursor(cursor);
106 setSuggestionsVisibility(cursor.getCount() > 0);
107 } else if (cursor != null) {
114 public void onCreate(Bundle savedInstanceState) {
115 super.onCreate(savedInstanceState);
117 mResultsAdapter = new SearchResultsAdapter(getActivity());
118 mSuggestionsAdapter = new SuggestionsAdapter(getActivity());
120 if (savedInstanceState != null) {
121 mShowResults = savedInstanceState.getBoolean(SAVE_KEY_SHOW_RESULTS);
126 public void onSaveInstanceState(Bundle outState) {
127 super.onSaveInstanceState(outState);
129 outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults);
133 public void onStop() {
141 public void onDestroy() {
142 mResultsListView = null;
143 mResultsAdapter = null;
144 mUpdateSearchResultsTask = null;
146 mSuggestionsListView = null;
147 mSuggestionsAdapter = null;
148 mUpdateSuggestionsTask = null;
156 public View onCreateView(LayoutInflater inflater, ViewGroup container,
157 Bundle savedInstanceState) {
159 final View view = inflater.inflate(R.layout.search_panel, container, false);
161 mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions);
162 mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results);
164 mResultsListView = (ListView) view.findViewById(R.id.list_results);
165 mResultsListView.setAdapter(mResultsAdapter);
166 mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
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
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"
178 final Cursor cursor = mResultsAdapter.mCursor;
179 cursor.moveToPosition(position);
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);
186 final SettingsActivity sa = (SettingsActivity) getActivity();
187 sa.needToRevertToInitialFragment();
189 if (TextUtils.isEmpty(action)) {
190 Bundle args = new Bundle();
191 args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
193 Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle);
195 final Intent intent = new Intent(action);
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);
206 intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
208 sa.startActivity(intent);
211 saveQueryToDatabase();
214 mResultsListView.addHeaderView(
215 LayoutInflater.from(getActivity()).inflate(
216 R.layout.search_panel_results_header, mResultsListView, false),
219 mSuggestionsListView = (ListView) view.findViewById(R.id.list_suggestions);
220 mSuggestionsListView.setAdapter(mSuggestionsAdapter);
221 mSuggestionsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
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
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"
231 final Cursor cursor = mSuggestionsAdapter.mCursor;
232 cursor.moveToPosition(position);
235 mQuery = cursor.getString(0);
236 mSearchView.setQuery(mQuery, false);
239 mSuggestionsListView.addHeaderView(
240 LayoutInflater.from(getActivity()).inflate(
241 R.layout.search_panel_suggestions_header, mSuggestionsListView, false),
248 public void onResume() {
252 showSomeSuggestions();
256 public void setSearchView(SearchView searchView) {
257 mSearchView = searchView;
260 private void setSuggestionsVisibility(boolean visible) {
261 if (mLayoutSuggestions != null) {
262 mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE);
266 private void setResultsVisibility(boolean visible) {
267 if (mLayoutResults != null) {
268 mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE);
272 private void saveQueryToDatabase() {
273 Index.getInstance(getActivity()).addSavedQuery(mQuery);
276 public boolean onQueryTextSubmit(String query) {
277 mQuery = getFilteredQueryString(query);
279 setSuggestionsVisibility(false);
280 updateSearchResults();
281 saveQueryToDatabase();
285 public boolean onQueryTextChange(String query) {
286 final String newQuery = getFilteredQueryString(query);
290 if (TextUtils.isEmpty(mQuery)) {
291 mShowResults = false;
292 setResultsVisibility(false);
296 setSuggestionsVisibility(false);
297 updateSearchResults();
303 public void showSomeSuggestions() {
304 setResultsVisibility(false);
305 mQuery = EMPTY_QUERY;
309 private void clearSuggestions() {
310 if (mUpdateSuggestionsTask != null) {
311 mUpdateSuggestionsTask.cancel(false);
312 mUpdateSuggestionsTask = null;
314 setSuggestionsCursor(null);
317 private void setSuggestionsCursor(Cursor cursor) {
318 if (mSuggestionsAdapter == null) {
321 Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor);
322 if (oldCursor != null) {
327 private void clearResults() {
328 if (mUpdateSearchResultsTask != null) {
329 mUpdateSearchResultsTask.cancel(false);
330 mUpdateSearchResultsTask = null;
332 setResultsCursor(null);
335 private void setResultsCursor(Cursor cursor) {
336 if (mResultsAdapter == null) {
339 Cursor oldCursor = mResultsAdapter.swapCursor(cursor);
340 if (oldCursor != null) {
345 private String getFilteredQueryString(CharSequence query) {
349 final StringBuilder filtered = new StringBuilder();
350 for (int n = 0; n < query.length(); n++) {
351 char c = query.charAt(n);
352 if (!Character.isLetterOrDigit(c) && !Character.isSpaceChar(c)) {
357 return filtered.toString();
360 private void clearAllTasks() {
361 if (mUpdateSearchResultsTask != null) {
362 mUpdateSearchResultsTask.cancel(false);
363 mUpdateSearchResultsTask = null;
365 if (mUpdateSuggestionsTask != null) {
366 mUpdateSuggestionsTask.cancel(false);
367 mUpdateSuggestionsTask = null;
371 private void updateSuggestions() {
373 if (mQuery == null) {
374 setSuggestionsCursor(null);
376 mUpdateSuggestionsTask = new UpdateSuggestionsTask();
377 mUpdateSuggestionsTask.execute(mQuery);
381 private void updateSearchResults() {
383 if (TextUtils.isEmpty(mQuery)) {
384 setResultsVisibility(false);
385 setResultsCursor(null);
387 mUpdateSearchResultsTask = new UpdateSearchResultsTask();
388 mUpdateSearchResultsTask.execute(mQuery);
392 private static class SuggestionItem {
395 public SuggestionItem(String query) {
400 private static class SuggestionsAdapter extends BaseAdapter {
402 private static final int COLUMN_SUGGESTION_QUERY = 0;
403 private static final int COLUMN_SUGGESTION_TIMESTAMP = 1;
405 private Context mContext;
406 private Cursor mCursor;
407 private LayoutInflater mInflater;
408 private boolean mDataValid = false;
410 public SuggestionsAdapter(Context context) {
412 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
416 public Cursor swapCursor(Cursor newCursor) {
417 if (newCursor == mCursor) {
420 Cursor oldCursor = mCursor;
422 if (newCursor != null) {
424 notifyDataSetChanged();
427 notifyDataSetInvalidated();
433 public int getCount() {
434 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
435 return mCursor.getCount();
439 public Object getItem(int position) {
440 if (mDataValid && mCursor.moveToPosition(position)) {
441 final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY);
443 return new SuggestionItem(query);
449 public long getItemId(int position) {
454 public View getView(int position, View convertView, ViewGroup parent) {
455 if (!mDataValid && convertView == null) {
456 throw new IllegalStateException(
457 "this should only be called when the cursor is valid");
459 if (!mCursor.moveToPosition(position)) {
460 throw new IllegalStateException("couldn't move cursor to position " + position);
465 if (convertView == null) {
466 view = mInflater.inflate(R.layout.search_suggestion_item, parent, false);
471 TextView query = (TextView) view.findViewById(R.id.title);
473 SuggestionItem item = (SuggestionItem) getItem(position);
474 query.setText(item.query);
480 private static class SearchResult {
481 public Context context;
483 public String summaryOn;
484 public String summaryOff;
485 public String entries;
486 public int iconResId;
489 public SearchResult(Context context, String title, String summaryOn, String summaryOff,
490 String entries, int iconResId, String key) {
491 this.context = context;
493 this.summaryOn = summaryOn;
494 this.summaryOff = summaryOff;
495 this.entries = entries;
496 this.iconResId = iconResId;
501 private static class SearchResultsAdapter extends BaseAdapter {
503 private Context mContext;
504 private Cursor mCursor;
505 private LayoutInflater mInflater;
506 private boolean mDataValid;
507 private HashMap<String, Context> mContextMap = new HashMap<String, Context>();
509 private static final String PERCENT_RECLACE = "%s";
510 private static final String DOLLAR_REPLACE = "$s";
512 public SearchResultsAdapter(Context context) {
514 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
518 public Cursor swapCursor(Cursor newCursor) {
519 if (newCursor == mCursor) {
522 Cursor oldCursor = mCursor;
524 if (newCursor != null) {
526 notifyDataSetChanged();
529 notifyDataSetInvalidated();
535 public int getCount() {
536 if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
537 return mCursor.getCount();
541 public Object getItem(int position) {
542 if (mDataValid && mCursor.moveToPosition(position)) {
543 final String title = mCursor.getString(Index.COLUMN_INDEX_TITLE);
544 final String summaryOn = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_ON);
545 final String summaryOff = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_OFF);
546 final String entries = mCursor.getString(Index.COLUMN_INDEX_ENTRIES);
547 final String iconResStr = mCursor.getString(Index.COLUMN_INDEX_ICON);
548 final String className = mCursor.getString(
549 Index.COLUMN_INDEX_CLASS_NAME);
550 final String packageName = mCursor.getString(
551 Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
552 final String key = mCursor.getString(
553 Index.COLUMN_INDEX_KEY);
555 Context packageContext;
556 if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) {
557 packageContext = mContextMap.get(packageName);
558 if (packageContext == null) {
560 packageContext = mContext.createPackageContext(packageName, 0);
561 } catch (PackageManager.NameNotFoundException e) {
562 Log.e(LOG_TAG, "Cannot create Context for package: " + packageName);
565 mContextMap.put(packageName, packageContext);
568 packageContext = mContext;
571 final int iconResId = TextUtils.isEmpty(iconResStr) ?
572 R.drawable.empty_icon : Integer.parseInt(iconResStr);
574 return new SearchResult(packageContext, title, summaryOn, summaryOff,
575 entries, iconResId, key);
581 public long getItemId(int position) {
586 public View getView(int position, View convertView, ViewGroup parent) {
587 if (!mDataValid && convertView == null) {
588 throw new IllegalStateException(
589 "this should only be called when the cursor is valid");
591 if (!mCursor.moveToPosition(position)) {
592 throw new IllegalStateException("couldn't move cursor to position " + position);
599 if (convertView == null) {
600 view = mInflater.inflate(R.layout.search_result_item, parent, false);
605 textTitle = (TextView) view.findViewById(R.id.title);
606 imageView = (ImageView) view.findViewById(R.id.icon);
608 final SearchResult result = (SearchResult) getItem(position);
609 textTitle.setText(result.title);
611 if (result.iconResId != R.drawable.empty_icon) {
612 final Context packageContext = result.context;
613 final Drawable drawable;
615 drawable = packageContext.getDrawable(result.iconResId);
616 imageView.setImageDrawable(drawable);
617 } catch (Resources.NotFoundException nfe) {
618 // Not much we can do except logging
619 Log.e(LOG_TAG, "Cannot load Drawable for " + result.title);
622 imageView.setImageDrawable(null);
623 imageView.setBackgroundResource(R.drawable.empty_icon);