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.app.Fragment;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.PackageManager;
24import android.content.res.Resources;
25import android.database.Cursor;
26import android.graphics.drawable.Drawable;
27import android.os.AsyncTask;
28import android.os.Bundle;
29import android.text.TextUtils;
30import android.util.Log;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.AdapterView;
35import android.widget.BaseAdapter;
36import android.widget.ImageView;
37import android.widget.ListView;
38import android.widget.SearchView;
39import android.widget.TextView;
40import com.android.settings.R;
41import com.android.settings.SettingsActivity;
42import com.android.settings.Utils;
43import com.android.settings.search.Index;
44
45import java.util.HashMap;
46
47public 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
78        protected Cursor doInBackground(String... params) {
79            return Index.getInstance(getActivity()).search(params[0]);
80        }
81
82        @Override
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
98        protected Cursor doInBackground(String... params) {
99            return Index.getInstance(getActivity()).getSuggestions(params[0]);
100        }
101
102        @Override
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
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
126    public void onSaveInstanceState(Bundle outState) {
127        super.onSaveInstanceState(outState);
128
129        outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults);
130    }
131
132    @Override
133    public void onStop() {
134        super.onStop();
135
136        clearSuggestions();
137        clearResults();
138    }
139
140    @Override
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
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
248    public void onResume() {
249        super.onResume();
250
251        if (!mShowResults) {
252            showSomeSuggestions();
253        }
254    }
255
256    public void setSearchView(SearchView searchView) {
257        mSearchView = searchView;
258    }
259
260    private void setSuggestionsVisibility(boolean visible) {
261        if (mLayoutSuggestions != null) {
262            mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE);
263        }
264    }
265
266    private void setResultsVisibility(boolean visible) {
267        if (mLayoutResults != null) {
268            mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE);
269        }
270    }
271
272    private void saveQueryToDatabase() {
273        Index.getInstance(getActivity()).addSavedQuery(mQuery);
274    }
275
276    public boolean onQueryTextSubmit(String query) {
277        mQuery = getFilteredQueryString(query);
278        mShowResults = true;
279        setSuggestionsVisibility(false);
280        updateSearchResults();
281        saveQueryToDatabase();
282        return true;
283    }
284
285    public boolean onQueryTextChange(String query) {
286        final String newQuery = getFilteredQueryString(query);
287
288        mQuery = newQuery;
289
290        if (TextUtils.isEmpty(mQuery)) {
291            mShowResults = false;
292            setResultsVisibility(false);
293            updateSuggestions();
294        } else {
295            mShowResults = true;
296            setSuggestionsVisibility(false);
297            updateSearchResults();
298        }
299
300        return true;
301    }
302
303    public void showSomeSuggestions() {
304        setResultsVisibility(false);
305        mQuery = EMPTY_QUERY;
306        updateSuggestions();
307    }
308
309    private void clearSuggestions() {
310        if (mUpdateSuggestionsTask != null) {
311            mUpdateSuggestionsTask.cancel(false);
312            mUpdateSuggestionsTask = null;
313        }
314        setSuggestionsCursor(null);
315    }
316
317    private void setSuggestionsCursor(Cursor cursor) {
318        if (mSuggestionsAdapter == null) {
319            return;
320        }
321        Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor);
322        if (oldCursor != null) {
323            oldCursor.close();
324        }
325    }
326
327    private void clearResults() {
328        if (mUpdateSearchResultsTask != null) {
329            mUpdateSearchResultsTask.cancel(false);
330            mUpdateSearchResultsTask = null;
331        }
332        setResultsCursor(null);
333    }
334
335    private void setResultsCursor(Cursor cursor) {
336        if (mResultsAdapter == null) {
337            return;
338        }
339        Cursor oldCursor = mResultsAdapter.swapCursor(cursor);
340        if (oldCursor != null) {
341            oldCursor.close();
342        }
343    }
344
345    private String getFilteredQueryString(CharSequence query) {
346        if (query == null) {
347            return null;
348        }
349        final StringBuilder filtered = new StringBuilder();
350        for (int n = 0; n < query.length(); n++) {
351            char c = query.charAt(n);
352            if (!Character.isLetterOrDigit(c) && !Character.isSpaceChar(c)) {
353                continue;
354            }
355            filtered.append(c);
356        }
357        return filtered.toString();
358    }
359
360    private void clearAllTasks() {
361        if (mUpdateSearchResultsTask != null) {
362            mUpdateSearchResultsTask.cancel(false);
363            mUpdateSearchResultsTask = null;
364        }
365        if (mUpdateSuggestionsTask != null) {
366            mUpdateSuggestionsTask.cancel(false);
367            mUpdateSuggestionsTask = null;
368        }
369    }
370
371    private void updateSuggestions() {
372        clearAllTasks();
373        if (mQuery == null) {
374            setSuggestionsCursor(null);
375        } else {
376            mUpdateSuggestionsTask = new UpdateSuggestionsTask();
377            mUpdateSuggestionsTask.execute(mQuery);
378        }
379    }
380
381    private void updateSearchResults() {
382        clearAllTasks();
383        if (TextUtils.isEmpty(mQuery)) {
384            setResultsVisibility(false);
385            setResultsCursor(null);
386        } else {
387            mUpdateSearchResultsTask = new UpdateSearchResultsTask();
388            mUpdateSearchResultsTask.execute(mQuery);
389        }
390    }
391
392    private static class SuggestionItem {
393        public String query;
394
395        public SuggestionItem(String query) {
396            this.query = query;
397        }
398    }
399
400    private static class SuggestionsAdapter extends BaseAdapter {
401
402        private static final int COLUMN_SUGGESTION_QUERY = 0;
403        private static final int COLUMN_SUGGESTION_TIMESTAMP = 1;
404
405        private Context mContext;
406        private Cursor mCursor;
407        private LayoutInflater mInflater;
408        private boolean mDataValid = false;
409
410        public SuggestionsAdapter(Context context) {
411            mContext = context;
412            mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
413            mDataValid = false;
414        }
415
416        public Cursor swapCursor(Cursor newCursor) {
417            if (newCursor == mCursor) {
418                return null;
419            }
420            Cursor oldCursor = mCursor;
421            mCursor = newCursor;
422            if (newCursor != null) {
423                mDataValid = true;
424                notifyDataSetChanged();
425            } else {
426                mDataValid = false;
427                notifyDataSetInvalidated();
428            }
429            return oldCursor;
430        }
431
432        @Override
433        public int getCount() {
434            if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
435            return mCursor.getCount();
436        }
437
438        @Override
439        public Object getItem(int position) {
440            if (mDataValid && mCursor.moveToPosition(position)) {
441                final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY);
442
443                return new SuggestionItem(query);
444            }
445            return null;
446        }
447
448        @Override
449        public long getItemId(int position) {
450            return 0;
451        }
452
453        @Override
454        public View getView(int position, View convertView, ViewGroup parent) {
455            if (!mDataValid && convertView == null) {
456                throw new IllegalStateException(
457                        "this should only be called when the cursor is valid");
458            }
459            if (!mCursor.moveToPosition(position)) {
460                throw new IllegalStateException("couldn't move cursor to position " + position);
461            }
462
463            View view;
464
465            if (convertView == null) {
466                view = mInflater.inflate(R.layout.search_suggestion_item, parent, false);
467            } else {
468                view = convertView;
469            }
470
471            TextView query = (TextView) view.findViewById(R.id.title);
472
473            SuggestionItem item = (SuggestionItem) getItem(position);
474            query.setText(item.query);
475
476            return view;
477        }
478    }
479
480    private static class SearchResult {
481        public Context context;
482        public String title;
483        public String summaryOn;
484        public String summaryOff;
485        public String entries;
486        public int iconResId;
487        public String key;
488
489        public SearchResult(Context context, String title, String summaryOn, String summaryOff,
490                            String entries, int iconResId, String key) {
491            this.context = context;
492            this.title = title;
493            this.summaryOn = summaryOn;
494            this.summaryOff = summaryOff;
495            this.entries = entries;
496            this.iconResId = iconResId;
497            this.key = key;
498        }
499    }
500
501    private static class SearchResultsAdapter extends BaseAdapter {
502
503        private Context mContext;
504        private Cursor mCursor;
505        private LayoutInflater mInflater;
506        private boolean mDataValid;
507        private HashMap<String, Context> mContextMap = new HashMap<String, Context>();
508
509        private static final String PERCENT_RECLACE = "%s";
510        private static final String DOLLAR_REPLACE = "$s";
511
512        public SearchResultsAdapter(Context context) {
513            mContext = context;
514            mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
515            mDataValid = false;
516        }
517
518        public Cursor swapCursor(Cursor newCursor) {
519            if (newCursor == mCursor) {
520                return null;
521            }
522            Cursor oldCursor = mCursor;
523            mCursor = newCursor;
524            if (newCursor != null) {
525                mDataValid = true;
526                notifyDataSetChanged();
527            } else {
528                mDataValid = false;
529                notifyDataSetInvalidated();
530            }
531            return oldCursor;
532        }
533
534        @Override
535        public int getCount() {
536            if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
537            return mCursor.getCount();
538        }
539
540        @Override
541        public Object getItem(int position) {
542            if (mDataValid && mCursor.moveToPosition(position)) {
543                final String title = mCursor.getString(Index.COLUMN_INDEX_TITLE);
544                final String summaryOn = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_ON);
545                final String summaryOff = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_OFF);
546                final String entries = mCursor.getString(Index.COLUMN_INDEX_ENTRIES);
547                final String iconResStr = mCursor.getString(Index.COLUMN_INDEX_ICON);
548                final String className = mCursor.getString(
549                        Index.COLUMN_INDEX_CLASS_NAME);
550                final String packageName = mCursor.getString(
551                        Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
552                final String key = mCursor.getString(
553                        Index.COLUMN_INDEX_KEY);
554
555                Context packageContext;
556                if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) {
557                    packageContext = mContextMap.get(packageName);
558                    if (packageContext == null) {
559                        try {
560                            packageContext = mContext.createPackageContext(packageName, 0);
561                        } catch (PackageManager.NameNotFoundException e) {
562                            Log.e(LOG_TAG, "Cannot create Context for package: " + packageName);
563                            return null;
564                        }
565                        mContextMap.put(packageName, packageContext);
566                    }
567                } else {
568                    packageContext = mContext;
569                }
570
571                final int iconResId = TextUtils.isEmpty(iconResStr) ?
572                        R.drawable.empty_icon : Integer.parseInt(iconResStr);
573
574                return new SearchResult(packageContext, title, summaryOn, summaryOff,
575                        entries, iconResId, key);
576            }
577            return null;
578        }
579
580        @Override
581        public long getItemId(int position) {
582            return 0;
583        }
584
585        @Override
586        public View getView(int position, View convertView, ViewGroup parent) {
587            if (!mDataValid && convertView == null) {
588                throw new IllegalStateException(
589                        "this should only be called when the cursor is valid");
590            }
591            if (!mCursor.moveToPosition(position)) {
592                throw new IllegalStateException("couldn't move cursor to position " + position);
593            }
594
595            View view;
596            TextView textTitle;
597            ImageView imageView;
598
599            if (convertView == null) {
600                view = mInflater.inflate(R.layout.search_result_item, parent, false);
601            } else {
602                view = convertView;
603            }
604
605            textTitle = (TextView) view.findViewById(R.id.title);
606            imageView = (ImageView) view.findViewById(R.id.icon);
607
608            final SearchResult result = (SearchResult) getItem(position);
609            textTitle.setText(result.title);
610
611            if (result.iconResId != R.drawable.empty_icon) {
612                final Context packageContext = result.context;
613                final Drawable drawable;
614                try {
615                    drawable = packageContext.getDrawable(result.iconResId);
616                    imageView.setImageDrawable(drawable);
617                } catch (Resources.NotFoundException nfe) {
618                    // Not much we can do except logging
619                    Log.e(LOG_TAG, "Cannot load Drawable for " + result.title);
620                }
621            } else {
622                imageView.setImageDrawable(null);
623                imageView.setBackgroundResource(R.drawable.empty_icon);
624            }
625
626            return view;
627        }
628    }
629}
630