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 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 
     80         public void onSearch(String txt);
     81 
     82         public void onSelect(String txt, int type, String extraData);
     83 
     84     }
     85 
     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 
     99     public void setLandscapeMode(boolean mode) {
    100         mLandscapeMode = mode;
    101         notifyDataSetChanged();
    102     }
    103 
    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
    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
    124     public Filter getFilter() {
    125         return mFilter;
    126     }
    127 
    128     @Override
    129     public int getCount() {
    130         return (mMixedResults == null) ? 0 : mMixedResults.getLineCount();
    131     }
    132 
    133     @Override
    134     public SuggestItem getItem(int position) {
    135         if (mMixedResults == null) {
    136             return null;
    137         }
    138         return mMixedResults.items.get(position);
    139     }
    140 
    141     @Override
    142     public long getItemId(int position) {
    143         return position;
    144     }
    145 
    146     @Override
    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 
    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
    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
    220         protected void onPostExecute(List<SuggestItem> items) {
    221             mSuggestResults = items;
    222             mMixedResults = buildSuggestionResults();
    223             notifyDataSetChanged();
    224         }
    225     }
    226 
    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
    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 
    262         void startSuggestionsAsync(final CharSequence constraint) {
    263             if (!mIncognitoMode) {
    264                 new SlowFilterTask().execute(constraint);
    265             }
    266         }
    267 
    268         private boolean shouldProcessEmptyQuery() {
    269             final SearchEngine searchEngine = mSettings.getSearchEngine();
    270             return searchEngine.wantsEmptyQuery();
    271         }
    272 
    273         @Override
    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 
    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
    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 
    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 
    337         SuggestionResults() {
    338             items = new ArrayList<SuggestItem>(24);
    339             // n of types:
    340             counts = new int[5];
    341         }
    342 
    343         int getTypeCount(int type) {
    344             return counts[type];
    345         }
    346 
    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 
    355         int getLineCount() {
    356             return Math.min((mLandscapeMode ? mLinesLandscape : mLinesPortrait), items.size());
    357         }
    358 
    359         @Override
    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 
    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 
    396         boolean moveToNext() {
    397             return mCursor.moveToNext();
    398         }
    399 
    400         public abstract void runQuery(CharSequence constraint);
    401 
    402         public abstract SuggestItem getItem();
    403 
    404         public int getCount() {
    405             return (mCursor != null) ? mCursor.getCount() : 0;
    406         }
    407 
    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
    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
    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          */
    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          */
    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
    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
    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 
    542     public void clearCache() {
    543         mFilterResults = null;
    544         mSuggestResults = null;
    545         notifyDataSetInvalidated();
    546     }
    547 
    548     public void setIncognitoMode(boolean incognito) {
    549         mIncognitoMode = incognito;
    550         clearCache();
    551     }
    552 
    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 
    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