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