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.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