OSDN Git Service

Merge tag 'android-6.0.1_r74' into HEAD
[android-x86/packages-apps-Settings.git] / src / com / android / settings / dashboard / SearchResultsSummary.java
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 import com.android.internal.logging.MetricsLogger;
40 import com.android.settings.InstrumentedFragment;
41 import com.android.settings.R;
42 import com.android.settings.SettingsActivity;
43 import com.android.settings.Utils;
44 import com.android.settings.search.Index;
45
46 import java.util.HashMap;
47
48 public class SearchResultsSummary extends InstrumentedFragment {
49
50     private static final String LOG_TAG = "SearchResultsSummary";
51
52     private static final String EMPTY_QUERY = "";
53     private static char ELLIPSIS = '\u2026';
54
55     private static final String SAVE_KEY_SHOW_RESULTS = ":settings:show_results";
56
57     private SearchView mSearchView;
58
59     private ListView mResultsListView;
60     private SearchResultsAdapter mResultsAdapter;
61     private UpdateSearchResultsTask mUpdateSearchResultsTask;
62
63     private ListView mSuggestionsListView;
64     private SuggestionsAdapter mSuggestionsAdapter;
65     private UpdateSuggestionsTask mUpdateSuggestionsTask;
66
67     private ViewGroup mLayoutSuggestions;
68     private ViewGroup mLayoutResults;
69
70     private String mQuery;
71
72     private boolean mShowResults;
73
74     /**
75      * A basic AsyncTask for updating the query results cursor
76      */
77     private class UpdateSearchResultsTask extends AsyncTask<String, Void, Cursor> {
78         @Override
79         protected Cursor doInBackground(String... params) {
80             return Index.getInstance(getActivity()).search(params[0]);
81         }
82
83         @Override
84         protected void onPostExecute(Cursor cursor) {
85             if (!isCancelled()) {
86                 MetricsLogger.action(getContext(), MetricsLogger.ACTION_SEARCH_RESULTS,
87                         cursor.getCount());
88                 setResultsCursor(cursor);
89                 setResultsVisibility(cursor.getCount() > 0);
90             } else if (cursor != null) {
91                 cursor.close();
92             }
93         }
94     }
95
96     /**
97      * A basic AsyncTask for updating the suggestions cursor
98      */
99     private class UpdateSuggestionsTask extends AsyncTask<String, Void, Cursor> {
100         @Override
101         protected Cursor doInBackground(String... params) {
102             return Index.getInstance(getActivity()).getSuggestions(params[0]);
103         }
104
105         @Override
106         protected void onPostExecute(Cursor cursor) {
107             if (!isCancelled()) {
108                 setSuggestionsCursor(cursor);
109                 setSuggestionsVisibility(cursor.getCount() > 0);
110             } else if (cursor != null) {
111                 cursor.close();
112             }
113         }
114     }
115
116     @Override
117     public void onCreate(Bundle savedInstanceState) {
118         super.onCreate(savedInstanceState);
119
120         mResultsAdapter = new SearchResultsAdapter(getActivity());
121         mSuggestionsAdapter = new SuggestionsAdapter(getActivity());
122
123         if (savedInstanceState != null) {
124             mShowResults = savedInstanceState.getBoolean(SAVE_KEY_SHOW_RESULTS);
125         }
126     }
127
128     @Override
129     public void onSaveInstanceState(Bundle outState) {
130         super.onSaveInstanceState(outState);
131
132         outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults);
133     }
134
135     @Override
136     public void onStop() {
137         super.onStop();
138
139         clearSuggestions();
140         clearResults();
141     }
142
143     @Override
144     public void onDestroy() {
145         mResultsListView = null;
146         mResultsAdapter = null;
147         mUpdateSearchResultsTask = null;
148
149         mSuggestionsListView = null;
150         mSuggestionsAdapter = null;
151         mUpdateSuggestionsTask = null;
152
153         mSearchView = null;
154
155         super.onDestroy();
156     }
157
158     @Override
159     public View onCreateView(LayoutInflater inflater, ViewGroup container,
160                              Bundle savedInstanceState) {
161
162         final View view = inflater.inflate(R.layout.search_panel, container, false);
163
164         mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions);
165         mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results);
166
167         mResultsListView = (ListView) view.findViewById(R.id.list_results);
168         mResultsListView.setAdapter(mResultsAdapter);
169         mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
170             @Override
171             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
172                 // We have a header, so we need to decrement the position by one
173                 position--;
174
175                 // Some Monkeys could create a case where they were probably clicking on the
176                 // List Header and thus the position passed was "0" and then by decrement was "-1"
177                 if (position < 0) {
178                     return;
179                 }
180
181                 final Cursor cursor = mResultsAdapter.mCursor;
182                 cursor.moveToPosition(position);
183
184                 final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME);
185                 final String screenTitle = cursor.getString(Index.COLUMN_INDEX_SCREEN_TITLE);
186                 final String action = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION);
187                 final String key = cursor.getString(Index.COLUMN_INDEX_KEY);
188
189                 final SettingsActivity sa = (SettingsActivity) getActivity();
190                 sa.needToRevertToInitialFragment();
191
192                 if (TextUtils.isEmpty(action)) {
193                     Bundle args = new Bundle();
194                     args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
195
196                     Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle);
197                 } else {
198                     final Intent intent = new Intent(action);
199
200                     final String targetPackage = cursor.getString(
201                             Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
202                     final String targetClass = cursor.getString(
203                             Index.COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS);
204                     if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) {
205                         final ComponentName component =
206                                 new ComponentName(targetPackage, targetClass);
207                         intent.setComponent(component);
208                     }
209                     intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
210
211                     sa.startActivity(intent);
212                 }
213
214                 saveQueryToDatabase();
215             }
216         });
217         mResultsListView.addHeaderView(
218                 LayoutInflater.from(getActivity()).inflate(
219                         R.layout.search_panel_results_header, mResultsListView, false),
220                 null, false);
221
222         mSuggestionsListView = (ListView) view.findViewById(R.id.list_suggestions);
223         mSuggestionsListView.setAdapter(mSuggestionsAdapter);
224         mSuggestionsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
225             @Override
226             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
227                 // We have a header, so we need to decrement the position by one
228                 position--;
229                 // Some Monkeys could create a case where they were probably clicking on the
230                 // List Header and thus the position passed was "0" and then by decrement was "-1"
231                 if (position < 0) {
232                     return;
233                 }
234                 final Cursor cursor = mSuggestionsAdapter.mCursor;
235                 cursor.moveToPosition(position);
236
237                 mShowResults = true;
238                 mQuery = cursor.getString(0);
239                 if (mSearchView != null) {
240                     mSearchView.setQuery(mQuery, false);
241                 }
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 MetricsLogger.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 }