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