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
doInBackground(String... params)81         protected Cursor doInBackground(String... params) {
82             return Index.getInstance(getActivity()).search(params[0]);
83         }
84 
85         @Override
onPostExecute(Cursor cursor)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
doInBackground(String... params)103         protected Cursor doInBackground(String... params) {
104             return Index.getInstance(getActivity()).getSuggestions(params[0]);
105         }
106 
107         @Override
onPostExecute(Cursor cursor)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
onCreate(Bundle savedInstanceState)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
onSaveInstanceState(Bundle outState)131     public void onSaveInstanceState(Bundle outState) {
132         super.onSaveInstanceState(outState);
133 
134         outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults);
135     }
136 
137     @Override
onStop()138     public void onStop() {
139         super.onStop();
140 
141         clearSuggestions();
142         clearResults();
143     }
144 
145     @Override
onDestroy()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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
getMetricsCategory()253     protected int getMetricsCategory() {
254         return MetricsEvent.DASHBOARD_SEARCH_RESULTS;
255     }
256 
257     @Override
onResume()258     public void onResume() {
259         super.onResume();
260 
261         if (!mShowResults) {
262             showSomeSuggestions();
263         }
264     }
265 
setSearchView(SearchView searchView)266     public void setSearchView(SearchView searchView) {
267         mSearchView = searchView;
268     }
269 
setSuggestionsVisibility(boolean visible)270     private void setSuggestionsVisibility(boolean visible) {
271         if (mLayoutSuggestions != null) {
272             mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE);
273         }
274     }
275 
setResultsVisibility(boolean visible)276     private void setResultsVisibility(boolean visible) {
277         if (mLayoutResults != null) {
278             mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE);
279         }
280     }
281 
saveQueryToDatabase()282     private void saveQueryToDatabase() {
283         Index.getInstance(getActivity()).addSavedQuery(mQuery);
284     }
285 
onQueryTextSubmit(String query)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 
onQueryTextChange(String query)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 
showSomeSuggestions()314     public void showSomeSuggestions() {
315         setResultsVisibility(false);
316         mQuery = EMPTY_QUERY;
317         updateSuggestions();
318     }
319 
clearSuggestions()320     private void clearSuggestions() {
321         if (mUpdateSuggestionsTask != null) {
322             mUpdateSuggestionsTask.cancel(false);
323             mUpdateSuggestionsTask = null;
324         }
325         setSuggestionsCursor(null);
326     }
327 
setSuggestionsCursor(Cursor cursor)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 
clearResults()338     private void clearResults() {
339         if (mUpdateSearchResultsTask != null) {
340             mUpdateSearchResultsTask.cancel(false);
341             mUpdateSearchResultsTask = null;
342         }
343         setResultsCursor(null);
344     }
345 
setResultsCursor(Cursor cursor)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 
getFilteredQueryString(CharSequence query)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 
clearAllTasks()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 
updateSuggestions()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 
updateSearchResults()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 
SuggestionItem(String query)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 
SuggestionsAdapter(Context context)421         public SuggestionsAdapter(Context context) {
422             mContext = context;
423             mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
424             mDataValid = false;
425         }
426 
swapCursor(Cursor newCursor)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
getCount()444         public int getCount() {
445             if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
446             return mCursor.getCount();
447         }
448 
449         @Override
getItem(int position)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
getItemId(int position)460         public long getItemId(int position) {
461             return 0;
462         }
463 
464         @Override
getView(int position, View convertView, ViewGroup parent)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 
SearchResult(Context context, String title, String summaryOn, String summaryOff, String entries, int iconResId, String key)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 
SearchResultsAdapter(Context context)523         public SearchResultsAdapter(Context context) {
524             mContext = context;
525             mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
526             mDataValid = false;
527         }
528 
swapCursor(Cursor newCursor)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
getCount()546         public int getCount() {
547             if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
548             return mCursor.getCount();
549         }
550 
551         @Override
getItem(int position)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
getItemId(int position)592         public long getItemId(int position) {
593             return 0;
594         }
595 
596         @Override
getView(int position, View convertView, ViewGroup parent)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