Home | History | Annotate | Download | only in browser
      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             SuggestCursor cursor = new SuggestCursor();
    227             cursor.runQuery(params[0]);
    228             List<SuggestItem> results = new ArrayList<SuggestItem>();
    229             int count = cursor.getCount();
    230             for (int i = 0; i < count; i++) {
    231                 results.add(cursor.getItem());
    232                 cursor.moveToNext();
    233             }
    234             cursor.close();
    235             return results;
    236         }
    237 
    238         @Override
    239         protected void onPostExecute(List<SuggestItem> items) {
    240             mSuggestResults = items;
    241             mMixedResults = buildSuggestionResults();
    242             notifyDataSetChanged();
    243         }
    244     }
    245 
    246     SuggestionResults buildSuggestionResults() {
    247         SuggestionResults mixed = new SuggestionResults();
    248         List<SuggestItem> filter, suggest;
    249         synchronized (mResultsLock) {
    250             filter = mFilterResults;
    251             suggest = mSuggestResults;
    252         }
    253         if (filter != null) {
    254             for (SuggestItem item : filter) {
    255                 mixed.addResult(item);
    256             }
    257         }
    258         if (suggest != null) {
    259             for (SuggestItem item : suggest) {
    260                 mixed.addResult(item);
    261             }
    262         }
    263         return mixed;
    264     }
    265 
    266     class SuggestFilter extends Filter {
    267 
    268         @Override
    269         public CharSequence convertResultToString(Object item) {
    270             if (item == null) {
    271                 return "";
    272             }
    273             SuggestItem sitem = (SuggestItem) item;
    274             if (sitem.title != null) {
    275                 return sitem.title;
    276             } else {
    277                 return sitem.url;
    278             }
    279         }
    280 
    281         void startSuggestionsAsync(final CharSequence constraint) {
    282             if (!mIncognitoMode) {
    283                 new SlowFilterTask().execute(constraint);
    284             }
    285         }
    286 
    287         private boolean shouldProcessEmptyQuery() {
    288             final SearchEngine searchEngine = mSettings.getSearchEngine();
    289             return searchEngine.wantsEmptyQuery();
    290         }
    291 
    292         @Override
    293         protected FilterResults performFiltering(CharSequence constraint) {
    294             FilterResults res = new FilterResults();
    295             if (mVoiceResults == null) {
    296                 if (TextUtils.isEmpty(constraint) && !shouldProcessEmptyQuery()) {
    297                     res.count = 0;
    298                     res.values = null;
    299                     return res;
    300                 }
    301                 startSuggestionsAsync(constraint);
    302                 List<SuggestItem> filterResults = new ArrayList<SuggestItem>();
    303                 if (constraint != null) {
    304                     for (CursorSource sc : mSources) {
    305                         sc.runQuery(constraint);
    306                     }
    307                     mixResults(filterResults);
    308                 }
    309                 synchronized (mResultsLock) {
    310                     mFilterResults = filterResults;
    311                 }
    312                 SuggestionResults mixed = buildSuggestionResults();
    313                 res.count = mixed.getLineCount();
    314                 res.values = mixed;
    315             } else {
    316                 res.count = mVoiceResults.size();
    317                 res.values = mVoiceResults;
    318             }
    319             return res;
    320         }
    321 
    322         void mixResults(List<SuggestItem> results) {
    323             int maxLines = getMaxLines();
    324             for (int i = 0; i < mSources.size(); i++) {
    325                 CursorSource s = mSources.get(i);
    326                 int n = Math.min(s.getCount(), maxLines);
    327                 maxLines -= n;
    328                 boolean more = false;
    329                 for (int j = 0; j < n; j++) {
    330                     results.add(s.getItem());
    331                     more = s.moveToNext();
    332                 }
    333             }
    334         }
    335 
    336         @Override
    337         protected void publishResults(CharSequence constraint, FilterResults fresults) {
    338             if (fresults.values instanceof SuggestionResults) {
    339                 mMixedResults = (SuggestionResults) fresults.values;
    340                 notifyDataSetChanged();
    341             }
    342         }
    343     }
    344 
    345     private int getMaxLines() {
    346         int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait;
    347         maxLines = (int) Math.ceil(maxLines / 2.0);
    348         return maxLines;
    349     }
    350 
    351     /**
    352      * sorted list of results of a suggestion query
    353      *
    354      */
    355     class SuggestionResults {
    356 
    357         ArrayList<SuggestItem> items;
    358         // count per type
    359         int[] counts;
    360 
    361         SuggestionResults() {
    362             items = new ArrayList<SuggestItem>(24);
    363             // n of types:
    364             counts = new int[5];
    365         }
    366 
    367         int getTypeCount(int type) {
    368             return counts[type];
    369         }
    370 
    371         void addResult(SuggestItem item) {
    372             int ix = 0;
    373             while ((ix < items.size()) && (item.type >= items.get(ix).type))
    374                 ix++;
    375             items.add(ix, item);
    376             counts[item.type]++;
    377         }
    378 
    379         int getLineCount() {
    380             return Math.min((mLandscapeMode ? mLinesLandscape : mLinesPortrait), items.size());
    381         }
    382 
    383         @Override
    384         public String toString() {
    385             if (items == null) return null;
    386             if (items.size() == 0) return "[]";
    387             StringBuilder sb = new StringBuilder();
    388             for (int i = 0; i < items.size(); i++) {
    389                 SuggestItem item = items.get(i);
    390                 sb.append(item.type + ": " + item.title);
    391                 if (i < items.size() - 1) {
    392                     sb.append(", ");
    393                 }
    394             }
    395             return sb.toString();
    396         }
    397     }
    398 
    399     /**
    400      * data object to hold suggestion values
    401      */
    402     public class SuggestItem {
    403         public String title;
    404         public String url;
    405         public int type;
    406         public String extra;
    407 
    408         public SuggestItem(String text, String u, int t) {
    409             title = text;
    410             url = u;
    411             type = t;
    412         }
    413 
    414     }
    415 
    416     abstract class CursorSource {
    417 
    418         Cursor mCursor;
    419 
    420         boolean moveToNext() {
    421             return mCursor.moveToNext();
    422         }
    423 
    424         public abstract void runQuery(CharSequence constraint);
    425 
    426         public abstract SuggestItem getItem();
    427 
    428         public int getCount() {
    429             return (mCursor != null) ? mCursor.getCount() : 0;
    430         }
    431 
    432         public void close() {
    433             if (mCursor != null) {
    434                 mCursor.close();
    435             }
    436         }
    437     }
    438 
    439     /**
    440      * combined bookmark & history source
    441      */
    442     class CombinedCursor extends CursorSource {
    443 
    444         @Override
    445         public SuggestItem getItem() {
    446             if ((mCursor != null) && (!mCursor.isAfterLast())) {
    447                 String title = mCursor.getString(1);
    448                 String url = mCursor.getString(2);
    449                 boolean isBookmark = (mCursor.getInt(3) == 1);
    450                 return new SuggestItem(getTitle(title, url), getUrl(title, url),
    451                         isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY);
    452             }
    453             return null;
    454         }
    455 
    456         @Override
    457         public void runQuery(CharSequence constraint) {
    458             // constraint != null
    459             if (mCursor != null) {
    460                 mCursor.close();
    461             }
    462             String like = constraint + "%";
    463             String[] args = null;
    464             String selection = null;
    465             if (like.startsWith("http") || like.startsWith("file")) {
    466                 args = new String[1];
    467                 args[0] = like;
    468                 selection = "url LIKE ?";
    469             } else {
    470                 args = new String[5];
    471                 args[0] = "http://" + like;
    472                 args[1] = "http://www." + like;
    473                 args[2] = "https://" + like;
    474                 args[3] = "https://www." + like;
    475                 // To match against titles.
    476                 args[4] = like;
    477                 selection = COMBINED_SELECTION;
    478             }
    479             Uri.Builder ub = OmniboxSuggestions.CONTENT_URI.buildUpon();
    480             ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
    481                     Integer.toString(Math.max(mLinesLandscape, mLinesPortrait)));
    482             mCursor =
    483                     mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION,
    484                             selection, (constraint != null) ? args : null, null);
    485             if (mCursor != null) {
    486                 mCursor.moveToFirst();
    487             }
    488         }
    489 
    490         /**
    491          * Provides the title (text line 1) for a browser suggestion, which should be the
    492          * webpage title. If the webpage title is empty, returns the stripped url instead.
    493          *
    494          * @return the title string to use
    495          */
    496         private String getTitle(String title, String url) {
    497             if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
    498                 title = UrlUtils.stripUrl(url);
    499             }
    500             return title;
    501         }
    502 
    503         /**
    504          * Provides the subtitle (text line 2) for a browser suggestion, which should be the
    505          * webpage url. If the webpage title is empty, then the url should go in the title
    506          * instead, and the subtitle should be empty, so this would return null.
    507          *
    508          * @return the subtitle string to use, or null if none
    509          */
    510         private String getUrl(String title, String url) {
    511             if (TextUtils.isEmpty(title)
    512                     || TextUtils.getTrimmedLength(title) == 0
    513                     || title.equals(url)) {
    514                 return null;
    515             } else {
    516                 return UrlUtils.stripUrl(url);
    517             }
    518         }
    519     }
    520 
    521     class SuggestCursor extends CursorSource {
    522 
    523         @Override
    524         public SuggestItem getItem() {
    525             if (mCursor != null) {
    526                 String title = mCursor.getString(
    527                         mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
    528                 String text2 = mCursor.getString(
    529                         mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2));
    530                 String url = mCursor.getString(
    531                         mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL));
    532                 String uri = mCursor.getString(
    533                         mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA));
    534                 int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL;
    535                 SuggestItem item = new SuggestItem(title, url, type);
    536                 item.extra = mCursor.getString(
    537                         mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA));
    538                 return item;
    539             }
    540             return null;
    541         }
    542 
    543         @Override
    544         public void runQuery(CharSequence constraint) {
    545             if (mCursor != null) {
    546                 mCursor.close();
    547             }
    548             SearchEngine searchEngine = mSettings.getSearchEngine();
    549             if (!TextUtils.isEmpty(constraint)) {
    550                 if (searchEngine != null && searchEngine.supportsSuggestions()) {
    551                     mCursor = searchEngine.getSuggestions(mContext, constraint.toString());
    552                     if (mCursor != null) {
    553                         mCursor.moveToFirst();
    554                     }
    555                 }
    556             } else {
    557                 if (searchEngine.wantsEmptyQuery()) {
    558                     mCursor = searchEngine.getSuggestions(mContext, "");
    559                 }
    560                 mCursor = null;
    561             }
    562         }
    563 
    564     }
    565 
    566     public void clearCache() {
    567         mFilterResults = null;
    568         mSuggestResults = null;
    569         notifyDataSetInvalidated();
    570     }
    571 
    572     public void setIncognitoMode(boolean incognito) {
    573         mIncognitoMode = incognito;
    574         clearCache();
    575     }
    576 
    577     static String getSuggestionTitle(SuggestItem item) {
    578         // There must be a better way to strip HTML from things.
    579         // This method is used in multiple places. It is also more
    580         // expensive than a standard html escaper.
    581         return (item.title != null) ? Html.fromHtml(item.title).toString() : null;
    582     }
    583 
    584     static String getSuggestionUrl(SuggestItem item) {
    585         final String title = SuggestionsAdapter.getSuggestionTitle(item);
    586 
    587         if (TextUtils.isEmpty(item.url)) {
    588             return title;
    589         }
    590 
    591         return item.url;
    592     }
    593 }
    594