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         return true;
    283     }
    284 
    285     public boolean onQueryTextChange(String query) {
    286         final String newQuery = getFilteredQueryString(query);
    287 
    288         mQuery = newQuery;
    289 
    290         if (TextUtils.isEmpty(mQuery)) {
    291             mShowResults = false;
    292             setResultsVisibility(false);
    293             updateSuggestions();
    294         } else {
    295             mShowResults = true;
    296             setSuggestionsVisibility(false);
    297             updateSearchResults();
    298         }
    299 
    300         return true;
    301     }
    302 
    303     public void showSomeSuggestions() {
    304         setResultsVisibility(false);
    305         mQuery = EMPTY_QUERY;
    306         updateSuggestions();
    307     }
    308 
    309     private void clearSuggestions() {
    310         if (mUpdateSuggestionsTask != null) {
    311             mUpdateSuggestionsTask.cancel(false);
    312             mUpdateSuggestionsTask = null;
    313         }
    314         setSuggestionsCursor(null);
    315     }
    316 
    317     private void setSuggestionsCursor(Cursor cursor) {
    318         if (mSuggestionsAdapter == null) {
    319             return;
    320         }
    321         Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor);
    322         if (oldCursor != null) {
    323             oldCursor.close();
    324         }
    325     }
    326 
    327     private void clearResults() {
    328         if (mUpdateSearchResultsTask != null) {
    329             mUpdateSearchResultsTask.cancel(false);
    330             mUpdateSearchResultsTask = null;
    331         }
    332         setResultsCursor(null);
    333     }
    334 
    335     private void setResultsCursor(Cursor cursor) {
    336         if (mResultsAdapter == null) {
    337             return;
    338         }
    339         Cursor oldCursor = mResultsAdapter.swapCursor(cursor);
    340         if (oldCursor != null) {
    341             oldCursor.close();
    342         }
    343     }
    344 
    345     private String getFilteredQueryString(CharSequence query) {
    346         if (query == null) {
    347             return null;
    348         }
    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)) {
    353                 continue;
    354             }
    355             filtered.append(c);
    356         }
    357         return filtered.toString();
    358     }
    359 
    360     private void clearAllTasks() {
    361         if (mUpdateSearchResultsTask != null) {
    362             mUpdateSearchResultsTask.cancel(false);
    363             mUpdateSearchResultsTask = null;
    364         }
    365         if (mUpdateSuggestionsTask != null) {
    366             mUpdateSuggestionsTask.cancel(false);
    367             mUpdateSuggestionsTask = null;
    368         }
    369     }
    370 
    371     private void updateSuggestions() {
    372         clearAllTasks();
    373         if (mQuery == null) {
    374             setSuggestionsCursor(null);
    375         } else {
    376             mUpdateSuggestionsTask = new UpdateSuggestionsTask();
    377             mUpdateSuggestionsTask.execute(mQuery);
    378         }
    379     }
    380 
    381     private void updateSearchResults() {
    382         clearAllTasks();
    383         if (TextUtils.isEmpty(mQuery)) {
    384             setResultsVisibility(false);
    385             setResultsCursor(null);
    386         } else {
    387             mUpdateSearchResultsTask = new UpdateSearchResultsTask();
    388             mUpdateSearchResultsTask.execute(mQuery);
    389         }
    390     }
    391 
    392     private static class SuggestionItem {
    393         public String query;
    394 
    395         public SuggestionItem(String query) {
    396             this.query = query;
    397         }
    398     }
    399 
    400     private static class SuggestionsAdapter extends BaseAdapter {
    401 
    402         private static final int COLUMN_SUGGESTION_QUERY = 0;
    403         private static final int COLUMN_SUGGESTION_TIMESTAMP = 1;
    404 
    405         private Context mContext;
    406         private Cursor mCursor;
    407         private LayoutInflater mInflater;
    408         private boolean mDataValid = false;
    409 
    410         public SuggestionsAdapter(Context context) {
    411             mContext = context;
    412             mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    413             mDataValid = false;
    414         }
    415 
    416         public Cursor swapCursor(Cursor newCursor) {
    417             if (newCursor == mCursor) {
    418                 return null;
    419             }
    420             Cursor oldCursor = mCursor;
    421             mCursor = newCursor;
    422             if (newCursor != null) {
    423                 mDataValid = true;
    424                 notifyDataSetChanged();
    425             } else {
    426                 mDataValid = false;
    427                 notifyDataSetInvalidated();
    428             }
    429             return oldCursor;
    430         }
    431 
    432         @Override
    433         public int getCount() {
    434             if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
    435             return mCursor.getCount();
    436         }
    437 
    438         @Override
    439         public Object getItem(int position) {
    440             if (mDataValid && mCursor.moveToPosition(position)) {
    441                 final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY);
    442 
    443                 return new SuggestionItem(query);
    444             }
    445             return null;
    446         }
    447 
    448         @Override
    449         public long getItemId(int position) {
    450             return 0;
    451         }
    452 
    453         @Override
    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");
    458             }
    459             if (!mCursor.moveToPosition(position)) {
    460                 throw new IllegalStateException("couldn't move cursor to position " + position);
    461             }
    462 
    463             View view;
    464 
    465             if (convertView == null) {
    466                 view = mInflater.inflate(R.layout.search_suggestion_item, parent, false);
    467             } else {
    468                 view = convertView;
    469             }
    470 
    471             TextView query = (TextView) view.findViewById(R.id.title);
    472 
    473             SuggestionItem item = (SuggestionItem) getItem(position);
    474             query.setText(item.query);
    475 
    476             return view;
    477         }
    478     }
    479 
    480     private static class SearchResult {
    481         public Context context;
    482         public String title;
    483         public String summaryOn;
    484         public String summaryOff;
    485         public String entries;
    486         public int iconResId;
    487         public String key;
    488 
    489         public SearchResult(Context context, String title, String summaryOn, String summaryOff,
    490                             String entries, int iconResId, String key) {
    491             this.context = context;
    492             this.title = title;
    493             this.summaryOn = summaryOn;
    494             this.summaryOff = summaryOff;
    495             this.entries = entries;
    496             this.iconResId = iconResId;
    497             this.key = key;
    498         }
    499     }
    500 
    501     private static class SearchResultsAdapter extends BaseAdapter {
    502 
    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>();
    508 
    509         private static final String PERCENT_RECLACE = "%s";
    510         private static final String DOLLAR_REPLACE = "$s";
    511 
    512         public SearchResultsAdapter(Context context) {
    513             mContext = context;
    514             mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    515             mDataValid = false;
    516         }
    517 
    518         public Cursor swapCursor(Cursor newCursor) {
    519             if (newCursor == mCursor) {
    520                 return null;
    521             }
    522             Cursor oldCursor = mCursor;
    523             mCursor = newCursor;
    524             if (newCursor != null) {
    525                 mDataValid = true;
    526                 notifyDataSetChanged();
    527             } else {
    528                 mDataValid = false;
    529                 notifyDataSetInvalidated();
    530             }
    531             return oldCursor;
    532         }
    533 
    534         @Override
    535         public int getCount() {
    536             if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
    537             return mCursor.getCount();
    538         }
    539 
    540         @Override
    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);
    554 
    555                 Context packageContext;
    556                 if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) {
    557                     packageContext = mContextMap.get(packageName);
    558                     if (packageContext == null) {
    559                         try {
    560                             packageContext = mContext.createPackageContext(packageName, 0);
    561                         } catch (PackageManager.NameNotFoundException e) {
    562                             Log.e(LOG_TAG, "Cannot create Context for package: " + packageName);
    563                             return null;
    564                         }
    565                         mContextMap.put(packageName, packageContext);
    566                     }
    567                 } else {
    568                     packageContext = mContext;
    569                 }
    570 
    571                 final int iconResId = TextUtils.isEmpty(iconResStr) ?
    572                         R.drawable.empty_icon : Integer.parseInt(iconResStr);
    573 
    574                 return new SearchResult(packageContext, title, summaryOn, summaryOff,
    575                         entries, iconResId, key);
    576             }
    577             return null;
    578         }
    579 
    580         @Override
    581         public long getItemId(int position) {
    582             return 0;
    583         }
    584 
    585         @Override
    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");
    590             }
    591             if (!mCursor.moveToPosition(position)) {
    592                 throw new IllegalStateException("couldn't move cursor to position " + position);
    593             }
    594 
    595             View view;
    596             TextView textTitle;
    597             ImageView imageView;
    598 
    599             if (convertView == null) {
    600                 view = mInflater.inflate(R.layout.search_result_item, parent, false);
    601             } else {
    602                 view = convertView;
    603             }
    604 
    605             textTitle = (TextView) view.findViewById(R.id.title);
    606             imageView = (ImageView) view.findViewById(R.id.icon);
    607 
    608             final SearchResult result = (SearchResult) getItem(position);
    609             textTitle.setText(result.title);
    610 
    611             if (result.iconResId != R.drawable.empty_icon) {
    612                 final Context packageContext = result.context;
    613                 final Drawable drawable;
    614                 try {
    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);
    620                 }
    621             } else {
    622                 imageView.setImageDrawable(null);
    623                 imageView.setBackgroundResource(R.drawable.empty_icon);
    624             }
    625 
    626             return view;
    627         }
    628     }
    629 }
    630