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