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
17package com.android.settings.dashboard;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.PackageManager;
23import android.content.res.Resources;
24import android.database.Cursor;
25import android.graphics.drawable.Drawable;
26import android.os.AsyncTask;
27import android.os.Bundle;
28import android.text.TextUtils;
29import android.util.Log;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.AdapterView;
34import android.widget.BaseAdapter;
35import android.widget.ImageView;
36import android.widget.ListView;
37import android.widget.SearchView;
38import android.widget.TextView;
39
40import com.android.internal.logging.MetricsLogger;
41import com.android.internal.logging.MetricsProto.MetricsEvent;
42import com.android.settings.InstrumentedFragment;
43import com.android.settings.R;
44import com.android.settings.SettingsActivity;
45import com.android.settings.Utils;
46import com.android.settings.search.Index;
47
48import java.util.HashMap;
49
50public 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