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