Home | History | Annotate | Download | only in dashboard
      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 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;
     45 
     46 import java.util.HashMap;
     47 
     48 public class SearchResultsSummary extends InstrumentedFragment {
     49 
     50     private static final String LOG_TAG = "SearchResultsSummary";
     51 
     52     private static final String EMPTY_QUERY = "";
     53     private static char ELLIPSIS = '\u2026';
     54 
     55     private static final String SAVE_KEY_SHOW_RESULTS = ":settings:show_results";
     56 
     57     private SearchView mSearchView;
     58 
     59     private ListView mResultsListView;
     60     private SearchResultsAdapter mResultsAdapter;
     61     private UpdateSearchResultsTask mUpdateSearchResultsTask;
     62 
     63     private ListView mSuggestionsListView;
     64     private SuggestionsAdapter mSuggestionsAdapter;
     65     private UpdateSuggestionsTask mUpdateSuggestionsTask;
     66 
     67     private ViewGroup mLayoutSuggestions;
     68     private ViewGroup mLayoutResults;
     69 
     70     private String mQuery;
     71 
     72     private boolean mShowResults;
     73 
     74     /**
     75      * A basic AsyncTask for updating the query results cursor
     76      */
     77     private class UpdateSearchResultsTask extends AsyncTask<String, Void, Cursor> {
     78         @Override
     79         protected Cursor doInBackground(String... params) {
     80             return Index.getInstance(getActivity()).search(params[0]);
     81         }
     82 
     83         @Override
     84         protected void onPostExecute(Cursor cursor) {
     85             if (!isCancelled()) {
     86                 MetricsLogger.action(getContext(), MetricsLogger.ACTION_SEARCH_RESULTS,
     87                         cursor.getCount());
     88                 setResultsCursor(cursor);
     89                 setResultsVisibility(cursor.getCount() > 0);
     90             } else if (cursor != null) {
     91                 cursor.close();
     92             }
     93         }
     94     }
     95 
     96     /**
     97      * A basic AsyncTask for updating the suggestions cursor
     98      */
     99     private class UpdateSuggestionsTask extends AsyncTask<String, Void, Cursor> {
    100         @Override
    101         protected Cursor doInBackground(String... params) {
    102             return Index.getInstance(getActivity()).getSuggestions(params[0]);
    103         }
    104 
    105         @Override
    106         protected void onPostExecute(Cursor cursor) {
    107             if (!isCancelled()) {
    108                 setSuggestionsCursor(cursor);
    109                 setSuggestionsVisibility(cursor.getCount() > 0);
    110             } else if (cursor != null) {
    111                 cursor.close();
    112             }
    113         }
    114     }
    115 
    116     @Override
    117     public void onCreate(Bundle savedInstanceState) {
    118         super.onCreate(savedInstanceState);
    119 
    120         mResultsAdapter = new SearchResultsAdapter(getActivity());
    121         mSuggestionsAdapter = new SuggestionsAdapter(getActivity());
    122 
    123         if (savedInstanceState != null) {
    124             mShowResults = savedInstanceState.getBoolean(SAVE_KEY_SHOW_RESULTS);
    125         }
    126     }
    127 
    128     @Override
    129     public void onSaveInstanceState(Bundle outState) {
    130         super.onSaveInstanceState(outState);
    131 
    132         outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults);
    133     }
    134 
    135     @Override
    136     public void onStop() {
    137         super.onStop();
    138 
    139         clearSuggestions();
    140         clearResults();
    141     }
    142 
    143     @Override
    144     public void onDestroy() {
    145         mResultsListView = null;
    146         mResultsAdapter = null;
    147         mUpdateSearchResultsTask = null;
    148 
    149         mSuggestionsListView = null;
    150         mSuggestionsAdapter = null;
    151         mUpdateSuggestionsTask = null;
    152 
    153         mSearchView = null;
    154 
    155         super.onDestroy();
    156     }
    157 
    158     @Override
    159     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    160                              Bundle savedInstanceState) {
    161 
    162         final View view = inflater.inflate(R.layout.search_panel, container, false);
    163 
    164         mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions);
    165         mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results);
    166 
    167         mResultsListView = (ListView) view.findViewById(R.id.list_results);
    168         mResultsListView.setAdapter(mResultsAdapter);
    169         mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    170             @Override
    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
    173                 position--;
    174 
    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"
    177                 if (position < 0) {
    178                     return;
    179                 }
    180 
    181                 final Cursor cursor = mResultsAdapter.mCursor;
    182                 cursor.moveToPosition(position);
    183 
    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);
    188 
    189                 final SettingsActivity sa = (SettingsActivity) getActivity();
    190                 sa.needToRevertToInitialFragment();
    191 
    192                 if (TextUtils.isEmpty(action)) {
    193                     Bundle args = new Bundle();
    194                     args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
    195 
    196                     Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle);
    197                 } else {
    198                     final Intent intent = new Intent(action);
    199 
    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);
    208                     }
    209                     intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
    210 
    211                     sa.startActivity(intent);
    212                 }
    213 
    214                 saveQueryToDatabase();
    215             }
    216         });
    217         mResultsListView.addHeaderView(
    218                 LayoutInflater.from(getActivity()).inflate(
    219                         R.layout.search_panel_results_header, mResultsListView, false),
    220                 null, false);
    221 
    222         mSuggestionsListView = (ListView) view.findViewById(R.id.list_suggestions);
    223         mSuggestionsListView.setAdapter(mSuggestionsAdapter);
    224         mSuggestionsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    225             @Override
    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
    228                 position--;
    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"
    231                 if (position < 0) {
    232                     return;
    233                 }
    234                 final Cursor cursor = mSuggestionsAdapter.mCursor;
    235                 cursor.moveToPosition(position);
    236 
    237                 mShowResults = true;
    238                 mQuery = cursor.getString(0);
    239                 mSearchView.setQuery(mQuery, false);
    240             }
    241         });
    242         mSuggestionsListView.addHeaderView(
    243                 LayoutInflater.from(getActivity()).inflate(
    244                         R.layout.search_panel_suggestions_header, mSuggestionsListView, false),
    245                 null, false);
    246 
    247         return view;
    248     }
    249 
    250     @Override
    251     protected int getMetricsCategory() {
    252         return MetricsLogger.DASHBOARD_SEARCH_RESULTS;
    253     }
    254 
    255     @Override
    256     public void onResume() {
    257         super.onResume();
    258 
    259         if (!mShowResults) {
    260             showSomeSuggestions();
    261         }
    262     }
    263 
    264     public void setSearchView(SearchView searchView) {
    265         mSearchView = searchView;
    266     }
    267 
    268     private void setSuggestionsVisibility(boolean visible) {
    269         if (mLayoutSuggestions != null) {
    270             mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE);
    271         }
    272     }
    273 
    274     private void setResultsVisibility(boolean visible) {
    275         if (mLayoutResults != null) {
    276             mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE);
    277         }
    278     }
    279 
    280     private void saveQueryToDatabase() {
    281         Index.getInstance(getActivity()).addSavedQuery(mQuery);
    282     }
    283 
    284     public boolean onQueryTextSubmit(String query) {
    285         mQuery = getFilteredQueryString(query);
    286         mShowResults = true;
    287         setSuggestionsVisibility(false);
    288         updateSearchResults();
    289         saveQueryToDatabase();
    290 
    291         return false;
    292     }
    293 
    294     public boolean onQueryTextChange(String query) {
    295         final String newQuery = getFilteredQueryString(query);
    296 
    297         mQuery = newQuery;
    298 
    299         if (TextUtils.isEmpty(mQuery)) {
    300             mShowResults = false;
    301             setResultsVisibility(false);
    302             updateSuggestions();
    303         } else {
    304             mShowResults = true;
    305             setSuggestionsVisibility(false);
    306             updateSearchResults();
    307         }
    308 
    309         return true;
    310     }
    311 
    312     public void showSomeSuggestions() {
    313         setResultsVisibility(false);
    314         mQuery = EMPTY_QUERY;
    315         updateSuggestions();
    316     }
    317 
    318     private void clearSuggestions() {
    319         if (mUpdateSuggestionsTask != null) {
    320             mUpdateSuggestionsTask.cancel(false);
    321             mUpdateSuggestionsTask = null;
    322         }
    323         setSuggestionsCursor(null);
    324     }
    325 
    326     private void setSuggestionsCursor(Cursor cursor) {
    327         if (mSuggestionsAdapter == null) {
    328             return;
    329         }
    330         Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor);
    331         if (oldCursor != null) {
    332             oldCursor.close();
    333         }
    334     }
    335 
    336     private void clearResults() {
    337         if (mUpdateSearchResultsTask != null) {
    338             mUpdateSearchResultsTask.cancel(false);
    339             mUpdateSearchResultsTask = null;
    340         }
    341         setResultsCursor(null);
    342     }
    343 
    344     private void setResultsCursor(Cursor cursor) {
    345         if (mResultsAdapter == null) {
    346             return;
    347         }
    348         Cursor oldCursor = mResultsAdapter.swapCursor(cursor);
    349         if (oldCursor != null) {
    350             oldCursor.close();
    351         }
    352     }
    353 
    354     private String getFilteredQueryString(CharSequence query) {
    355         if (query == null) {
    356             return null;
    357         }
    358         final StringBuilder filtered = new StringBuilder();
    359         for (int n = 0; n < query.length(); n++) {
    360             char c = query.charAt(n);
    361             if (!Character.isLetterOrDigit(c) && !Character.isSpaceChar(c)) {
    362                 continue;
    363             }
    364             filtered.append(c);
    365         }
    366         return filtered.toString();
    367     }
    368 
    369     private void clearAllTasks() {
    370         if (mUpdateSearchResultsTask != null) {
    371             mUpdateSearchResultsTask.cancel(false);
    372             mUpdateSearchResultsTask = null;
    373         }
    374         if (mUpdateSuggestionsTask != null) {
    375             mUpdateSuggestionsTask.cancel(false);
    376             mUpdateSuggestionsTask = null;
    377         }
    378     }
    379 
    380     private void updateSuggestions() {
    381         clearAllTasks();
    382         if (mQuery == null) {
    383             setSuggestionsCursor(null);
    384         } else {
    385             mUpdateSuggestionsTask = new UpdateSuggestionsTask();
    386             mUpdateSuggestionsTask.execute(mQuery);
    387         }
    388     }
    389 
    390     private void updateSearchResults() {
    391         clearAllTasks();
    392         if (TextUtils.isEmpty(mQuery)) {
    393             setResultsVisibility(false);
    394             setResultsCursor(null);
    395         } else {
    396             mUpdateSearchResultsTask = new UpdateSearchResultsTask();
    397             mUpdateSearchResultsTask.execute(mQuery);
    398         }
    399     }
    400 
    401     private static class SuggestionItem {
    402         public String query;
    403 
    404         public SuggestionItem(String query) {
    405             this.query = query;
    406         }
    407     }
    408 
    409     private static class SuggestionsAdapter extends BaseAdapter {
    410 
    411         private static final int COLUMN_SUGGESTION_QUERY = 0;
    412         private static final int COLUMN_SUGGESTION_TIMESTAMP = 1;
    413 
    414         private Context mContext;
    415         private Cursor mCursor;
    416         private LayoutInflater mInflater;
    417         private boolean mDataValid = false;
    418 
    419         public SuggestionsAdapter(Context context) {
    420             mContext = context;
    421             mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    422             mDataValid = false;
    423         }
    424 
    425         public Cursor swapCursor(Cursor newCursor) {
    426             if (newCursor == mCursor) {
    427                 return null;
    428             }
    429             Cursor oldCursor = mCursor;
    430             mCursor = newCursor;
    431             if (newCursor != null) {
    432                 mDataValid = true;
    433                 notifyDataSetChanged();
    434             } else {
    435                 mDataValid = false;
    436                 notifyDataSetInvalidated();
    437             }
    438             return oldCursor;
    439         }
    440 
    441         @Override
    442         public int getCount() {
    443             if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
    444             return mCursor.getCount();
    445         }
    446 
    447         @Override
    448         public Object getItem(int position) {
    449             if (mDataValid && mCursor.moveToPosition(position)) {
    450                 final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY);
    451 
    452                 return new SuggestionItem(query);
    453             }
    454             return null;
    455         }
    456 
    457         @Override
    458         public long getItemId(int position) {
    459             return 0;
    460         }
    461 
    462         @Override
    463         public View getView(int position, View convertView, ViewGroup parent) {
    464             if (!mDataValid && convertView == null) {
    465                 throw new IllegalStateException(
    466                         "this should only be called when the cursor is valid");
    467             }
    468             if (!mCursor.moveToPosition(position)) {
    469                 throw new IllegalStateException("couldn't move cursor to position " + position);
    470             }
    471 
    472             View view;
    473 
    474             if (convertView == null) {
    475                 view = mInflater.inflate(R.layout.search_suggestion_item, parent, false);
    476             } else {
    477                 view = convertView;
    478             }
    479 
    480             TextView query = (TextView) view.findViewById(R.id.title);
    481 
    482             SuggestionItem item = (SuggestionItem) getItem(position);
    483             query.setText(item.query);
    484 
    485             return view;
    486         }
    487     }
    488 
    489     private static class SearchResult {
    490         public Context context;
    491         public String title;
    492         public String summaryOn;
    493         public String summaryOff;
    494         public String entries;
    495         public int iconResId;
    496         public String key;
    497 
    498         public SearchResult(Context context, String title, String summaryOn, String summaryOff,
    499                             String entries, int iconResId, String key) {
    500             this.context = context;
    501             this.title = title;
    502             this.summaryOn = summaryOn;
    503             this.summaryOff = summaryOff;
    504             this.entries = entries;
    505             this.iconResId = iconResId;
    506             this.key = key;
    507         }
    508     }
    509 
    510     private static class SearchResultsAdapter extends BaseAdapter {
    511 
    512         private Context mContext;
    513         private Cursor mCursor;
    514         private LayoutInflater mInflater;
    515         private boolean mDataValid;
    516         private HashMap<String, Context> mContextMap = new HashMap<String, Context>();
    517 
    518         private static final String PERCENT_RECLACE = "%s";
    519         private static final String DOLLAR_REPLACE = "$s";
    520 
    521         public SearchResultsAdapter(Context context) {
    522             mContext = context;
    523             mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    524             mDataValid = false;
    525         }
    526 
    527         public Cursor swapCursor(Cursor newCursor) {
    528             if (newCursor == mCursor) {
    529                 return null;
    530             }
    531             Cursor oldCursor = mCursor;
    532             mCursor = newCursor;
    533             if (newCursor != null) {
    534                 mDataValid = true;
    535                 notifyDataSetChanged();
    536             } else {
    537                 mDataValid = false;
    538                 notifyDataSetInvalidated();
    539             }
    540             return oldCursor;
    541         }
    542 
    543         @Override
    544         public int getCount() {
    545             if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
    546             return mCursor.getCount();
    547         }
    548 
    549         @Override
    550         public Object getItem(int position) {
    551             if (mDataValid && mCursor.moveToPosition(position)) {
    552                 final String title = mCursor.getString(Index.COLUMN_INDEX_TITLE);
    553                 final String summaryOn = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_ON);
    554                 final String summaryOff = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_OFF);
    555                 final String entries = mCursor.getString(Index.COLUMN_INDEX_ENTRIES);
    556                 final String iconResStr = mCursor.getString(Index.COLUMN_INDEX_ICON);
    557                 final String className = mCursor.getString(
    558                         Index.COLUMN_INDEX_CLASS_NAME);
    559                 final String packageName = mCursor.getString(
    560                         Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
    561                 final String key = mCursor.getString(
    562                         Index.COLUMN_INDEX_KEY);
    563 
    564                 Context packageContext;
    565                 if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) {
    566                     packageContext = mContextMap.get(packageName);
    567                     if (packageContext == null) {
    568                         try {
    569                             packageContext = mContext.createPackageContext(packageName, 0);
    570                         } catch (PackageManager.NameNotFoundException e) {
    571                             Log.e(LOG_TAG, "Cannot create Context for package: " + packageName);
    572                             return null;
    573                         }
    574                         mContextMap.put(packageName, packageContext);
    575                     }
    576                 } else {
    577                     packageContext = mContext;
    578                 }
    579 
    580                 final int iconResId = TextUtils.isEmpty(iconResStr) ?
    581                         R.drawable.empty_icon : Integer.parseInt(iconResStr);
    582 
    583                 return new SearchResult(packageContext, title, summaryOn, summaryOff,
    584                         entries, iconResId, key);
    585             }
    586             return null;
    587         }
    588 
    589         @Override
    590         public long getItemId(int position) {
    591             return 0;
    592         }
    593 
    594         @Override
    595         public View getView(int position, View convertView, ViewGroup parent) {
    596             if (!mDataValid && convertView == null) {
    597                 throw new IllegalStateException(
    598                         "this should only be called when the cursor is valid");
    599             }
    600             if (!mCursor.moveToPosition(position)) {
    601                 throw new IllegalStateException("couldn't move cursor to position " + position);
    602             }
    603 
    604             View view;
    605             TextView textTitle;
    606             ImageView imageView;
    607 
    608             if (convertView == null) {
    609                 view = mInflater.inflate(R.layout.search_result_item, parent, false);
    610             } else {
    611                 view = convertView;
    612             }
    613 
    614             textTitle = (TextView) view.findViewById(R.id.title);
    615             imageView = (ImageView) view.findViewById(R.id.icon);
    616 
    617             final SearchResult result = (SearchResult) getItem(position);
    618             textTitle.setText(result.title);
    619 
    620             if (result.iconResId != R.drawable.empty_icon) {
    621                 final Context packageContext = result.context;
    622                 final Drawable drawable;
    623                 try {
    624                     drawable = packageContext.getDrawable(result.iconResId);
    625                     imageView.setImageDrawable(drawable);
    626                 } catch (Resources.NotFoundException nfe) {
    627                     // Not much we can do except logging
    628                     Log.e(LOG_TAG, "Cannot load Drawable for " + result.title);
    629                 }
    630             } else {
    631                 imageView.setImageDrawable(null);
    632                 imageView.setBackgroundResource(R.drawable.empty_icon);
    633             }
    634 
    635             return view;
    636         }
    637     }
    638 }
    639