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
doInBackground(String... params)78         protected Cursor doInBackground(String... params) {
79             return Index.getInstance(getActivity()).search(params[0]);
80         }
81 
82         @Override
onPostExecute(Cursor cursor)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
doInBackground(String... params)98         protected Cursor doInBackground(String... params) {
99             return Index.getInstance(getActivity()).getSuggestions(params[0]);
100         }
101 
102         @Override
onPostExecute(Cursor cursor)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
onCreate(Bundle savedInstanceState)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
onSaveInstanceState(Bundle outState)126     public void onSaveInstanceState(Bundle outState) {
127         super.onSaveInstanceState(outState);
128 
129         outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults);
130     }
131 
132     @Override
onStop()133     public void onStop() {
134         super.onStop();
135 
136         clearSuggestions();
137         clearResults();
138     }
139 
140     @Override
onDestroy()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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onResume()248     public void onResume() {
249         super.onResume();
250 
251         if (!mShowResults) {
252             showSomeSuggestions();
253         }
254     }
255 
setSearchView(SearchView searchView)256     public void setSearchView(SearchView searchView) {
257         mSearchView = searchView;
258     }
259 
setSuggestionsVisibility(boolean visible)260     private void setSuggestionsVisibility(boolean visible) {
261         if (mLayoutSuggestions != null) {
262             mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE);
263         }
264     }
265 
setResultsVisibility(boolean visible)266     private void setResultsVisibility(boolean visible) {
267         if (mLayoutResults != null) {
268             mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE);
269         }
270     }
271 
saveQueryToDatabase()272     private void saveQueryToDatabase() {
273         Index.getInstance(getActivity()).addSavedQuery(mQuery);
274     }
275 
onQueryTextSubmit(String query)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 
onQueryTextChange(String query)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 
showSomeSuggestions()304     public void showSomeSuggestions() {
305         setResultsVisibility(false);
306         mQuery = EMPTY_QUERY;
307         updateSuggestions();
308     }
309 
clearSuggestions()310     private void clearSuggestions() {
311         if (mUpdateSuggestionsTask != null) {
312             mUpdateSuggestionsTask.cancel(false);
313             mUpdateSuggestionsTask = null;
314         }
315         setSuggestionsCursor(null);
316     }
317 
setSuggestionsCursor(Cursor cursor)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 
clearResults()328     private void clearResults() {
329         if (mUpdateSearchResultsTask != null) {
330             mUpdateSearchResultsTask.cancel(false);
331             mUpdateSearchResultsTask = null;
332         }
333         setResultsCursor(null);
334     }
335 
setResultsCursor(Cursor cursor)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 
getFilteredQueryString(CharSequence query)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 
clearAllTasks()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 
updateSuggestions()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 
updateSearchResults()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 
SuggestionItem(String query)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 
SuggestionsAdapter(Context context)411         public SuggestionsAdapter(Context context) {
412             mContext = context;
413             mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
414             mDataValid = false;
415         }
416 
swapCursor(Cursor newCursor)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
getCount()434         public int getCount() {
435             if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
436             return mCursor.getCount();
437         }
438 
439         @Override
getItem(int position)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
getItemId(int position)450         public long getItemId(int position) {
451             return 0;
452         }
453 
454         @Override
getView(int position, View convertView, ViewGroup parent)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 
SearchResult(Context context, String title, String summaryOn, String summaryOff, String entries, int iconResId, String key)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 
SearchResultsAdapter(Context context)513         public SearchResultsAdapter(Context context) {
514             mContext = context;
515             mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
516             mDataValid = false;
517         }
518 
swapCursor(Cursor newCursor)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
getCount()536         public int getCount() {
537             if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
538             return mCursor.getCount();
539         }
540 
541         @Override
getItem(int position)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
getItemId(int position)582         public long getItemId(int position) {
583             return 0;
584         }
585 
586         @Override
getView(int position, View convertView, ViewGroup parent)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