SuggestionsAdapter.java revision 35defffe93d70e9d5b37ff550968debdcd5d3d8b
1/*
2 * Copyright (C) 2010 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.browser;
18
19import com.android.browser.search.SearchEngine;
20
21import android.app.SearchManager;
22import android.content.Context;
23import android.database.Cursor;
24import android.net.Uri;
25import android.os.AsyncTask;
26import android.os.Handler;
27import android.provider.BrowserContract;
28import android.text.TextUtils;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.View.OnClickListener;
32import android.view.ViewGroup;
33import android.widget.BaseAdapter;
34import android.widget.Filter;
35import android.widget.Filterable;
36import android.widget.ImageView;
37import android.widget.TextView;
38
39import java.util.ArrayList;
40import java.util.List;
41
42/**
43 * adapter to wrap multiple cursors for url/search completions
44 */
45public class SuggestionsAdapter extends BaseAdapter implements Filterable, OnClickListener {
46
47    static final int TYPE_BOOKMARK = 0;
48    static final int TYPE_SUGGEST_URL = 1;
49    static final int TYPE_HISTORY = 2;
50    static final int TYPE_SEARCH = 3;
51    static final int TYPE_SUGGEST = 4;
52
53    private static final String[] COMBINED_PROJECTION =
54            {BrowserContract.Combined._ID, BrowserContract.Combined.TITLE,
55                    BrowserContract.Combined.URL, BrowserContract.Combined.IS_BOOKMARK};
56
57    private static final String[] SEARCHES_PROJECTION = {BrowserContract.Searches.SEARCH};
58
59    private static final String COMBINED_SELECTION =
60            "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)";
61
62    Context mContext;
63    Filter mFilter;
64    SuggestionResults mMixedResults;
65    List<SuggestItem> mSuggestResults, mFilterResults;
66    List<CursorSource> mSources;
67    boolean mLandscapeMode;
68    CompletionListener mListener;
69    int mLinesPortrait;
70    int mLinesLandscape;
71    Object mResultsLock = new Object();
72
73    interface CompletionListener {
74
75        public void onSearch(String txt);
76
77        public void onSelect(String txt, String extraData);
78
79        public void onFilterComplete(int count);
80
81    }
82
83    public SuggestionsAdapter(Context ctx, CompletionListener listener) {
84        mContext = ctx;
85        mListener = listener;
86        mLinesPortrait = mContext.getResources().
87                getInteger(R.integer.max_suggest_lines_portrait);
88        mLinesLandscape = mContext.getResources().
89                getInteger(R.integer.max_suggest_lines_landscape);
90        mFilter = new SuggestFilter();
91        addSource(new SearchesCursor());
92        addSource(new CombinedCursor());
93    }
94
95    public void setLandscapeMode(boolean mode) {
96        mLandscapeMode = mode;
97        notifyDataSetChanged();
98    }
99
100    public int getLeftCount() {
101        return mMixedResults.getLeftCount();
102    }
103
104    public int getRightCount() {
105        return mMixedResults.getRightCount();
106    }
107
108    public void addSource(CursorSource c) {
109        if (mSources == null) {
110            mSources = new ArrayList<CursorSource>(5);
111        }
112        mSources.add(c);
113    }
114
115    @Override
116    public void onClick(View v) {
117        if (R.id.icon2 == v.getId()) {
118            // replace input field text with suggestion text
119            SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag();
120            mListener.onSearch(item.title);
121        } else {
122            SuggestItem item = (SuggestItem) v.getTag();
123            mListener.onSelect((TextUtils.isEmpty(item.url)? item.title : item.url),
124                    item.extra);
125        }
126    }
127
128    @Override
129    public Filter getFilter() {
130        return mFilter;
131    }
132
133    @Override
134    public int getCount() {
135        return (mMixedResults == null) ? 0 : mMixedResults.getLineCount();
136    }
137
138    @Override
139    public SuggestItem getItem(int position) {
140        if (mMixedResults == null) {
141            return null;
142        }
143        if (mLandscapeMode) {
144            if (position >= mMixedResults.getLineCount()) {
145                // right column
146                position = position - mMixedResults.getLineCount();
147                // index in column
148                if (position >= mMixedResults.getRightCount()) {
149                    return null;
150                }
151                return mMixedResults.items.get(position + mMixedResults.getLeftCount());
152            } else {
153                // left column
154                if (position >= mMixedResults.getLeftCount()) {
155                    return null;
156                }
157                return mMixedResults.items.get(position);
158            }
159        } else {
160            return mMixedResults.items.get(position);
161        }
162    }
163
164    @Override
165    public long getItemId(int position) {
166        return 0;
167    }
168
169    @Override
170    public View getView(int position, View convertView, ViewGroup parent) {
171        final LayoutInflater inflater = LayoutInflater.from(mContext);
172        View view = convertView;
173        if (view == null) {
174            view = inflater.inflate(R.layout.suggestion_two_column, parent, false);
175        }
176        View s1 = view.findViewById(R.id.suggest1);
177        View s2 = view.findViewById(R.id.suggest2);
178        View div = view.findViewById(R.id.suggestion_divider);
179        if (mLandscapeMode) {
180            SuggestItem item = getItem(position);
181            div.setVisibility(View.VISIBLE);
182            if (item != null) {
183                s1.setVisibility(View.VISIBLE);
184                bindView(s1, item);
185            } else {
186                s1.setVisibility(View.INVISIBLE);
187            }
188            item = getItem(position + mMixedResults.getLineCount());
189            if (item != null) {
190                s2.setVisibility(View.VISIBLE);
191                bindView(s2, item);
192            } else {
193                s2.setVisibility(View.INVISIBLE);
194            }
195            return view;
196        } else {
197            s1.setVisibility(View.VISIBLE);
198            div.setVisibility(View.GONE);
199            s2.setVisibility(View.GONE);
200            bindView(s1, getItem(position));
201            return view;
202        }
203    }
204
205    private void bindView(View view, SuggestItem item) {
206        // store item for click handling
207        view.setTag(item);
208        TextView tv1 = (TextView) view.findViewById(android.R.id.text1);
209        TextView tv2 = (TextView) view.findViewById(android.R.id.text2);
210        ImageView ic1 = (ImageView) view.findViewById(R.id.icon1);
211        View spacer = view.findViewById(R.id.spacer);
212        View ic2 = view.findViewById(R.id.icon2);
213        View div = view.findViewById(R.id.divider);
214        tv1.setText(item.title);
215        tv2.setText(item.url);
216        int id = -1;
217        switch (item.type) {
218            case TYPE_SUGGEST:
219            case TYPE_SEARCH:
220                id = R.drawable.ic_search_category_suggest;
221                break;
222            case TYPE_BOOKMARK:
223                id = R.drawable.ic_search_category_bookmark;
224                break;
225            case TYPE_HISTORY:
226                id = R.drawable.ic_search_category_history;
227                break;
228            case TYPE_SUGGEST_URL:
229                id = R.drawable.ic_search_category_browser;
230                break;
231            default:
232                id = -1;
233        }
234        if (id != -1) {
235            ic1.setImageDrawable(mContext.getResources().getDrawable(id));
236        }
237        ic2.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type))
238                ? View.VISIBLE : View.GONE);
239        div.setVisibility(ic2.getVisibility());
240        spacer.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type))
241                ? View.GONE : View.INVISIBLE);
242        view.setOnClickListener(this);
243        ic2.setOnClickListener(this);
244    }
245
246    class SlowFilterTask extends AsyncTask<CharSequence, Void, List<SuggestItem>> {
247
248        @Override
249        protected List<SuggestItem> doInBackground(CharSequence... params) {
250            SuggestCursor cursor = new SuggestCursor();
251            cursor.runQuery(params[0]);
252            List<SuggestItem> results = new ArrayList<SuggestItem>();
253            int count = cursor.getCount();
254            for (int i = 0; i < count; i++) {
255                results.add(cursor.getItem());
256                cursor.moveToNext();
257            }
258            cursor.close();
259            return results;
260        }
261
262        @Override
263        protected void onPostExecute(List<SuggestItem> items) {
264            mSuggestResults = items;
265            mMixedResults = buildSuggestionResults();
266            notifyDataSetChanged();
267            mListener.onFilterComplete(mMixedResults.getLineCount());
268        }
269    }
270
271    SuggestionResults buildSuggestionResults() {
272        SuggestionResults mixed = new SuggestionResults();
273        List<SuggestItem> filter, suggest;
274        synchronized (mResultsLock) {
275            filter = mFilterResults;
276            suggest = mSuggestResults;
277        }
278        if (filter != null) {
279            for (SuggestItem item : filter) {
280                mixed.addResult(item);
281            }
282        }
283        if (suggest != null) {
284            for (SuggestItem item : suggest) {
285                mixed.addResult(item);
286            }
287        }
288        return mixed;
289    }
290
291    class SuggestFilter extends Filter {
292
293        @Override
294        public CharSequence convertResultToString(Object item) {
295            if (item == null) {
296                return "";
297            }
298            SuggestItem sitem = (SuggestItem) item;
299            if (sitem.title != null) {
300                return sitem.title;
301            } else {
302                return sitem.url;
303            }
304        }
305
306        void startSuggestionsAsync(final CharSequence constraint) {
307            new SlowFilterTask().execute(constraint);
308        }
309
310        @Override
311        protected FilterResults performFiltering(CharSequence constraint) {
312            FilterResults res = new FilterResults();
313            if (TextUtils.isEmpty(constraint)) {
314                res.count = 0;
315                res.values = null;
316                return res;
317            }
318            startSuggestionsAsync(constraint);
319            List<SuggestItem> filterResults = new ArrayList<SuggestItem>();
320            if (constraint != null) {
321                for (CursorSource sc : mSources) {
322                    sc.runQuery(constraint);
323                }
324                mixResults(filterResults);
325            }
326            synchronized (mResultsLock) {
327                mFilterResults = filterResults;
328            }
329            SuggestionResults mixed = buildSuggestionResults();
330            res.count = mixed.getLineCount();
331            res.values = mixed;
332            return res;
333        }
334
335        void mixResults(List<SuggestItem> results) {
336            int maxLines = mLandscapeMode ? mLinesLandscape : (mLinesPortrait / 2);
337            for (int i = 0; i < mSources.size(); i++) {
338                CursorSource s = mSources.get(i);
339                int n = Math.min(s.getCount(), maxLines);
340                maxLines -= n;
341                boolean more = false;
342                for (int j = 0; j < n; j++) {
343                    results.add(s.getItem());
344                    more = s.moveToNext();
345                }
346            }
347        }
348
349        @Override
350        protected void publishResults(CharSequence constraint, FilterResults fresults) {
351            mMixedResults = (SuggestionResults) fresults.values;
352            mListener.onFilterComplete(fresults.count);
353            notifyDataSetChanged();
354        }
355
356    }
357
358    /**
359     * sorted list of results of a suggestion query
360     *
361     */
362    class SuggestionResults {
363
364        ArrayList<SuggestItem> items;
365        // count per type
366        int[] counts;
367
368        SuggestionResults() {
369            items = new ArrayList<SuggestItem>(24);
370            // n of types:
371            counts = new int[5];
372        }
373
374        int getTypeCount(int type) {
375            return counts[type];
376        }
377
378        void addResult(SuggestItem item) {
379            int ix = 0;
380            while ((ix < items.size()) && (item.type >= items.get(ix).type))
381                ix++;
382            items.add(ix, item);
383            counts[item.type]++;
384        }
385
386        int getLineCount() {
387            if (mLandscapeMode) {
388                return Math.min(mLinesLandscape,
389                        Math.max(getLeftCount(), getRightCount()));
390            } else {
391                return Math.min(mLinesPortrait, getLeftCount() + getRightCount());
392            }
393        }
394
395        int getLeftCount() {
396            return counts[TYPE_BOOKMARK] + counts[TYPE_HISTORY] + counts[TYPE_SUGGEST_URL];
397        }
398
399        int getRightCount() {
400            return counts[TYPE_SEARCH] + counts[TYPE_SUGGEST];
401        }
402
403        @Override
404        public String toString() {
405            if (items == null) return null;
406            if (items.size() == 0) return "[]";
407            StringBuilder sb = new StringBuilder();
408            for (int i = 0; i < items.size(); i++) {
409                SuggestItem item = items.get(i);
410                sb.append(item.type + ": " + item.title);
411                if (i < items.size() - 1) {
412                    sb.append(", ");
413                }
414            }
415            return sb.toString();
416        }
417    }
418
419    /**
420     * data object to hold suggestion values
421     */
422    class SuggestItem {
423        String title;
424        String url;
425        int type;
426        String extra;
427
428        public SuggestItem(String text, String u, int t) {
429            title = text;
430            url = u;
431            type = t;
432        }
433    }
434
435    abstract class CursorSource {
436
437        Cursor mCursor;
438
439        boolean moveToNext() {
440            return mCursor.moveToNext();
441        }
442
443        public abstract void runQuery(CharSequence constraint);
444
445        public abstract SuggestItem getItem();
446
447        public int getCount() {
448            return (mCursor != null) ? mCursor.getCount() : 0;
449        }
450
451        public void close() {
452            if (mCursor != null) {
453                mCursor.close();
454            }
455        }
456    }
457
458    /**
459     * combined bookmark & history source
460     */
461    class CombinedCursor extends CursorSource {
462
463        @Override
464        public SuggestItem getItem() {
465            if ((mCursor != null) && (!mCursor.isAfterLast())) {
466                String title = mCursor.getString(1);
467                String url = mCursor.getString(2);
468                boolean isBookmark = (mCursor.getInt(3) == 1);
469                return new SuggestItem(getTitle(title, url), getUrl(title, url),
470                        isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY);
471            }
472            return null;
473        }
474
475        @Override
476        public void runQuery(CharSequence constraint) {
477            // constraint != null
478            if (mCursor != null) {
479                mCursor.close();
480            }
481            String like = constraint + "%";
482            String[] args = null;
483            String selection = null;
484            if (like.startsWith("http") || like.startsWith("file")) {
485                args = new String[1];
486                args[0] = like;
487                selection = "url LIKE ?";
488            } else {
489                args = new String[5];
490                args[0] = "http://" + like;
491                args[1] = "http://www." + like;
492                args[2] = "https://" + like;
493                args[3] = "https://www." + like;
494                // To match against titles.
495                args[4] = like;
496                selection = COMBINED_SELECTION;
497            }
498            Uri.Builder ub = BrowserContract.Combined.CONTENT_URI.buildUpon();
499            ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
500                    Integer.toString(mLinesPortrait));
501            mCursor =
502                    mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION,
503                            selection,
504                            (constraint != null) ? args : null,
505                            BrowserContract.Combined.VISITS + " DESC, " +
506                            BrowserContract.Combined.DATE_LAST_VISITED + " DESC");
507            if (mCursor != null) {
508                mCursor.moveToFirst();
509            }
510        }
511
512        /**
513         * Provides the title (text line 1) for a browser suggestion, which should be the
514         * webpage title. If the webpage title is empty, returns the stripped url instead.
515         *
516         * @return the title string to use
517         */
518        private String getTitle(String title, String url) {
519            if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
520                title = UrlUtils.stripUrl(url);
521            }
522            return title;
523        }
524
525        /**
526         * Provides the subtitle (text line 2) for a browser suggestion, which should be the
527         * webpage url. If the webpage title is empty, then the url should go in the title
528         * instead, and the subtitle should be empty, so this would return null.
529         *
530         * @return the subtitle string to use, or null if none
531         */
532        private String getUrl(String title, String url) {
533            if (TextUtils.isEmpty(title)
534                    || TextUtils.getTrimmedLength(title) == 0
535                    || title.equals(url)) {
536                return null;
537            } else {
538                return UrlUtils.stripUrl(url);
539            }
540        }
541    }
542
543    class SearchesCursor extends CursorSource {
544
545        @Override
546        public SuggestItem getItem() {
547            if ((mCursor != null) && (!mCursor.isAfterLast())) {
548                return new SuggestItem(mCursor.getString(0), null, TYPE_SEARCH);
549            }
550            return null;
551        }
552
553        @Override
554        public void runQuery(CharSequence constraint) {
555            // constraint != null
556            if (mCursor != null) {
557                mCursor.close();
558            }
559            String like = constraint + "%";
560            String[] args = new String[] {like};
561            String selection = BrowserContract.Searches.SEARCH + " LIKE ?";
562            Uri.Builder ub = BrowserContract.Searches.CONTENT_URI.buildUpon();
563            ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
564                    Integer.toString(mLinesPortrait));
565            mCursor =
566                    mContext.getContentResolver().query(ub.build(), SEARCHES_PROJECTION,
567                            selection,
568                            args, BrowserContract.Searches.DATE + " DESC");
569            if (mCursor != null) {
570                mCursor.moveToFirst();
571            }
572        }
573
574    }
575
576    class SuggestCursor extends CursorSource {
577
578        @Override
579        public SuggestItem getItem() {
580            if (mCursor != null) {
581                String title = mCursor.getString(
582                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
583                String text2 = mCursor.getString(
584                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2));
585                String url = mCursor.getString(
586                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL));
587                String uri = mCursor.getString(
588                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA));
589                int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL;
590                SuggestItem item = new SuggestItem(title, url, type);
591                item.extra = mCursor.getString(
592                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA));
593                return item;
594            }
595            return null;
596        }
597
598        @Override
599        public void runQuery(CharSequence constraint) {
600            if (mCursor != null) {
601                mCursor.close();
602            }
603            if (!TextUtils.isEmpty(constraint)) {
604                SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine();
605                if (searchEngine != null && searchEngine.supportsSuggestions()) {
606                    mCursor = searchEngine.getSuggestions(mContext, constraint.toString());
607                    if (mCursor != null) {
608                        mCursor.moveToFirst();
609                    }
610                }
611            } else {
612                mCursor = null;
613            }
614        }
615
616    }
617
618    public void clearCache() {
619        mFilterResults = null;
620        mSuggestResults = null;
621    }
622
623}
624