| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.browser; |
| |
| import android.app.SearchManager; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.provider.BrowserContract; |
| import android.text.Html; |
| import android.text.TextUtils; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.widget.BaseAdapter; |
| import android.widget.Filter; |
| import android.widget.Filterable; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import com.android.browser.provider.BrowserProvider2.OmniboxSuggestions; |
| import com.android.browser.search.SearchEngine; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * adapter to wrap multiple cursors for url/search completions |
| */ |
| public class SuggestionsAdapter extends BaseAdapter implements Filterable, |
| OnClickListener { |
| |
| public static final int TYPE_BOOKMARK = 0; |
| public static final int TYPE_HISTORY = 1; |
| public static final int TYPE_SUGGEST_URL = 2; |
| public static final int TYPE_SEARCH = 3; |
| public static final int TYPE_SUGGEST = 4; |
| |
| private static final String[] COMBINED_PROJECTION = { |
| OmniboxSuggestions._ID, |
| OmniboxSuggestions.TITLE, |
| OmniboxSuggestions.URL, |
| OmniboxSuggestions.IS_BOOKMARK |
| }; |
| |
| private static final String COMBINED_SELECTION = |
| "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)"; |
| |
| final Context mContext; |
| final Filter mFilter; |
| SuggestionResults mMixedResults; |
| List<SuggestItem> mSuggestResults, mFilterResults; |
| List<CursorSource> mSources; |
| boolean mLandscapeMode; |
| final CompletionListener mListener; |
| final int mLinesPortrait; |
| final int mLinesLandscape; |
| final Object mResultsLock = new Object(); |
| boolean mIncognitoMode; |
| BrowserSettings mSettings; |
| |
| interface CompletionListener { |
| |
| public void onSearch(String txt); |
| |
| public void onSelect(String txt, int type, String extraData); |
| |
| } |
| |
| public SuggestionsAdapter(Context ctx, CompletionListener listener) { |
| mContext = ctx; |
| mSettings = BrowserSettings.getInstance(); |
| mListener = listener; |
| mLinesPortrait = mContext.getResources(). |
| getInteger(R.integer.max_suggest_lines_portrait); |
| mLinesLandscape = mContext.getResources(). |
| getInteger(R.integer.max_suggest_lines_landscape); |
| |
| mFilter = new SuggestFilter(); |
| addSource(new CombinedCursor()); |
| } |
| |
| public void setLandscapeMode(boolean mode) { |
| mLandscapeMode = mode; |
| notifyDataSetChanged(); |
| } |
| |
| public void addSource(CursorSource c) { |
| if (mSources == null) { |
| mSources = new ArrayList<CursorSource>(5); |
| } |
| mSources.add(c); |
| } |
| |
| @Override |
| public void onClick(View v) { |
| SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag(); |
| |
| if (R.id.icon2 == v.getId()) { |
| // replace input field text with suggestion text |
| mListener.onSearch(getSuggestionUrl(item)); |
| } else { |
| mListener.onSelect(getSuggestionUrl(item), item.type, item.extra); |
| } |
| } |
| |
| @Override |
| public Filter getFilter() { |
| return mFilter; |
| } |
| |
| @Override |
| public int getCount() { |
| return (mMixedResults == null) ? 0 : mMixedResults.getLineCount(); |
| } |
| |
| @Override |
| public SuggestItem getItem(int position) { |
| if (mMixedResults == null) { |
| return null; |
| } |
| return mMixedResults.items.get(position); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| final LayoutInflater inflater = LayoutInflater.from(mContext); |
| View view = convertView; |
| if (view == null) { |
| view = inflater.inflate(R.layout.suggestion_item, parent, false); |
| } |
| bindView(view, getItem(position)); |
| return view; |
| } |
| |
| private void bindView(View view, SuggestItem item) { |
| // store item for click handling |
| view.setTag(item); |
| TextView tv1 = (TextView) view.findViewById(android.R.id.text1); |
| TextView tv2 = (TextView) view.findViewById(android.R.id.text2); |
| ImageView ic1 = (ImageView) view.findViewById(R.id.icon1); |
| View ic2 = view.findViewById(R.id.icon2); |
| View div = view.findViewById(R.id.divider); |
| tv1.setText(Html.fromHtml(item.title)); |
| if (TextUtils.isEmpty(item.url)) { |
| tv2.setVisibility(View.GONE); |
| tv1.setMaxLines(2); |
| } else { |
| tv2.setVisibility(View.VISIBLE); |
| tv2.setText(item.url); |
| tv1.setMaxLines(1); |
| } |
| int id = -1; |
| switch (item.type) { |
| case TYPE_SUGGEST: |
| case TYPE_SEARCH: |
| id = R.drawable.ic_search_category_suggest; |
| break; |
| case TYPE_BOOKMARK: |
| id = R.drawable.ic_search_category_bookmark; |
| break; |
| case TYPE_HISTORY: |
| id = R.drawable.ic_search_category_history; |
| break; |
| case TYPE_SUGGEST_URL: |
| id = R.drawable.ic_search_category_browser; |
| break; |
| default: |
| id = -1; |
| } |
| if (id != -1) { |
| ic1.setImageDrawable(mContext.getResources().getDrawable(id)); |
| } |
| ic2.setVisibility(((TYPE_SUGGEST == item.type) |
| || (TYPE_SEARCH == item.type)) |
| ? View.VISIBLE : View.GONE); |
| div.setVisibility(ic2.getVisibility()); |
| ic2.setOnClickListener(this); |
| view.findViewById(R.id.suggestion).setOnClickListener(this); |
| } |
| |
| class SlowFilterTask extends AsyncTask<CharSequence, Void, List<SuggestItem>> { |
| |
| @Override |
| protected List<SuggestItem> doInBackground(CharSequence... params) { |
| SuggestCursor cursor = new SuggestCursor(); |
| cursor.runQuery(params[0]); |
| List<SuggestItem> results = new ArrayList<SuggestItem>(); |
| int count = cursor.getCount(); |
| for (int i = 0; i < count; i++) { |
| results.add(cursor.getItem()); |
| cursor.moveToNext(); |
| } |
| cursor.close(); |
| return results; |
| } |
| |
| @Override |
| protected void onPostExecute(List<SuggestItem> items) { |
| mSuggestResults = items; |
| mMixedResults = buildSuggestionResults(); |
| notifyDataSetChanged(); |
| } |
| } |
| |
| SuggestionResults buildSuggestionResults() { |
| SuggestionResults mixed = new SuggestionResults(); |
| List<SuggestItem> filter, suggest; |
| synchronized (mResultsLock) { |
| filter = mFilterResults; |
| suggest = mSuggestResults; |
| } |
| if (filter != null) { |
| for (SuggestItem item : filter) { |
| mixed.addResult(item); |
| } |
| } |
| if (suggest != null) { |
| for (SuggestItem item : suggest) { |
| mixed.addResult(item); |
| } |
| } |
| return mixed; |
| } |
| |
| class SuggestFilter extends Filter { |
| |
| @Override |
| public CharSequence convertResultToString(Object item) { |
| if (item == null) { |
| return ""; |
| } |
| SuggestItem sitem = (SuggestItem) item; |
| if (sitem.title != null) { |
| return sitem.title; |
| } else { |
| return sitem.url; |
| } |
| } |
| |
| void startSuggestionsAsync(final CharSequence constraint) { |
| if (!mIncognitoMode) { |
| new SlowFilterTask().execute(constraint); |
| } |
| } |
| |
| private boolean shouldProcessEmptyQuery() { |
| final SearchEngine searchEngine = mSettings.getSearchEngine(); |
| return searchEngine.wantsEmptyQuery(); |
| } |
| |
| @Override |
| protected FilterResults performFiltering(CharSequence constraint) { |
| FilterResults res = new FilterResults(); |
| if (TextUtils.isEmpty(constraint) && !shouldProcessEmptyQuery()) { |
| res.count = 0; |
| res.values = null; |
| return res; |
| } |
| startSuggestionsAsync(constraint); |
| List<SuggestItem> filterResults = new ArrayList<SuggestItem>(); |
| if (constraint != null) { |
| for (CursorSource sc : mSources) { |
| sc.runQuery(constraint); |
| } |
| mixResults(filterResults); |
| } |
| synchronized (mResultsLock) { |
| mFilterResults = filterResults; |
| } |
| SuggestionResults mixed = buildSuggestionResults(); |
| res.count = mixed.getLineCount(); |
| res.values = mixed; |
| return res; |
| } |
| |
| void mixResults(List<SuggestItem> results) { |
| int maxLines = getMaxLines(); |
| for (int i = 0; i < mSources.size(); i++) { |
| CursorSource s = mSources.get(i); |
| int n = Math.min(s.getCount(), maxLines); |
| maxLines -= n; |
| boolean more = false; |
| for (int j = 0; j < n; j++) { |
| results.add(s.getItem()); |
| more = s.moveToNext(); |
| } |
| } |
| } |
| |
| @Override |
| protected void publishResults(CharSequence constraint, FilterResults fresults) { |
| if (fresults.values instanceof SuggestionResults) { |
| mMixedResults = (SuggestionResults) fresults.values; |
| notifyDataSetChanged(); |
| } |
| } |
| } |
| |
| private int getMaxLines() { |
| int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait; |
| maxLines = (int) Math.ceil(maxLines / 2.0); |
| return maxLines; |
| } |
| |
| /** |
| * sorted list of results of a suggestion query |
| * |
| */ |
| class SuggestionResults { |
| |
| ArrayList<SuggestItem> items; |
| // count per type |
| int[] counts; |
| |
| SuggestionResults() { |
| items = new ArrayList<SuggestItem>(24); |
| // n of types: |
| counts = new int[5]; |
| } |
| |
| int getTypeCount(int type) { |
| return counts[type]; |
| } |
| |
| void addResult(SuggestItem item) { |
| int ix = 0; |
| while ((ix < items.size()) && (item.type >= items.get(ix).type)) |
| ix++; |
| items.add(ix, item); |
| counts[item.type]++; |
| } |
| |
| int getLineCount() { |
| return Math.min((mLandscapeMode ? mLinesLandscape : mLinesPortrait), items.size()); |
| } |
| |
| @Override |
| public String toString() { |
| if (items == null) return null; |
| if (items.size() == 0) return "[]"; |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < items.size(); i++) { |
| SuggestItem item = items.get(i); |
| sb.append(item.type + ": " + item.title); |
| if (i < items.size() - 1) { |
| sb.append(", "); |
| } |
| } |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * data object to hold suggestion values |
| */ |
| public class SuggestItem { |
| public String title; |
| public String url; |
| public int type; |
| public String extra; |
| |
| public SuggestItem(String text, String u, int t) { |
| title = text; |
| url = u; |
| type = t; |
| } |
| |
| } |
| |
| abstract class CursorSource { |
| |
| Cursor mCursor; |
| |
| boolean moveToNext() { |
| return mCursor.moveToNext(); |
| } |
| |
| public abstract void runQuery(CharSequence constraint); |
| |
| public abstract SuggestItem getItem(); |
| |
| public int getCount() { |
| return (mCursor != null) ? mCursor.getCount() : 0; |
| } |
| |
| public void close() { |
| if (mCursor != null) { |
| mCursor.close(); |
| } |
| } |
| } |
| |
| /** |
| * combined bookmark & history source |
| */ |
| class CombinedCursor extends CursorSource { |
| |
| @Override |
| public SuggestItem getItem() { |
| if ((mCursor != null) && (!mCursor.isAfterLast())) { |
| String title = mCursor.getString(1); |
| String url = mCursor.getString(2); |
| boolean isBookmark = (mCursor.getInt(3) == 1); |
| return new SuggestItem(getTitle(title, url), getUrl(title, url), |
| isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY); |
| } |
| return null; |
| } |
| |
| @Override |
| public void runQuery(CharSequence constraint) { |
| // constraint != null |
| if (mCursor != null) { |
| mCursor.close(); |
| } |
| String like = constraint + "%"; |
| String[] args = null; |
| String selection = null; |
| if (like.startsWith("http") || like.startsWith("file")) { |
| args = new String[1]; |
| args[0] = like; |
| selection = "url LIKE ?"; |
| } else { |
| args = new String[5]; |
| args[0] = "http://" + like; |
| args[1] = "http://www." + like; |
| args[2] = "https://" + like; |
| args[3] = "https://www." + like; |
| // To match against titles. |
| args[4] = like; |
| selection = COMBINED_SELECTION; |
| } |
| Uri.Builder ub = OmniboxSuggestions.CONTENT_URI.buildUpon(); |
| ub.appendQueryParameter(BrowserContract.PARAM_LIMIT, |
| Integer.toString(Math.max(mLinesLandscape, mLinesPortrait))); |
| mCursor = |
| mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION, |
| selection, (constraint != null) ? args : null, null); |
| if (mCursor != null) { |
| mCursor.moveToFirst(); |
| } |
| } |
| |
| /** |
| * Provides the title (text line 1) for a browser suggestion, which should be the |
| * webpage title. If the webpage title is empty, returns the stripped url instead. |
| * |
| * @return the title string to use |
| */ |
| private String getTitle(String title, String url) { |
| if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) { |
| title = UrlUtils.stripUrl(url); |
| } |
| return title; |
| } |
| |
| /** |
| * Provides the subtitle (text line 2) for a browser suggestion, which should be the |
| * webpage url. If the webpage title is empty, then the url should go in the title |
| * instead, and the subtitle should be empty, so this would return null. |
| * |
| * @return the subtitle string to use, or null if none |
| */ |
| private String getUrl(String title, String url) { |
| if (TextUtils.isEmpty(title) |
| || TextUtils.getTrimmedLength(title) == 0 |
| || title.equals(url)) { |
| return null; |
| } else { |
| return UrlUtils.stripUrl(url); |
| } |
| } |
| } |
| |
| class SuggestCursor extends CursorSource { |
| |
| @Override |
| public SuggestItem getItem() { |
| if (mCursor != null) { |
| String title = mCursor.getString( |
| mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)); |
| String text2 = mCursor.getString( |
| mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2)); |
| String url = mCursor.getString( |
| mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL)); |
| String uri = mCursor.getString( |
| mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA)); |
| int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL; |
| SuggestItem item = new SuggestItem(title, url, type); |
| item.extra = mCursor.getString( |
| mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA)); |
| return item; |
| } |
| return null; |
| } |
| |
| @Override |
| public void runQuery(CharSequence constraint) { |
| if (mCursor != null) { |
| mCursor.close(); |
| } |
| SearchEngine searchEngine = mSettings.getSearchEngine(); |
| if (!TextUtils.isEmpty(constraint)) { |
| if (searchEngine != null && searchEngine.supportsSuggestions()) { |
| mCursor = searchEngine.getSuggestions(mContext, constraint.toString()); |
| if (mCursor != null) { |
| mCursor.moveToFirst(); |
| } |
| } |
| } else { |
| if (searchEngine.wantsEmptyQuery()) { |
| mCursor = searchEngine.getSuggestions(mContext, ""); |
| } |
| mCursor = null; |
| } |
| } |
| |
| } |
| |
| public void clearCache() { |
| mFilterResults = null; |
| mSuggestResults = null; |
| notifyDataSetInvalidated(); |
| } |
| |
| public void setIncognitoMode(boolean incognito) { |
| mIncognitoMode = incognito; |
| clearCache(); |
| } |
| |
| static String getSuggestionTitle(SuggestItem item) { |
| // There must be a better way to strip HTML from things. |
| // This method is used in multiple places. It is also more |
| // expensive than a standard html escaper. |
| return (item.title != null) ? Html.fromHtml(item.title).toString() : null; |
| } |
| |
| static String getSuggestionUrl(SuggestItem item) { |
| final String title = SuggestionsAdapter.getSuggestionTitle(item); |
| |
| if (TextUtils.isEmpty(item.url)) { |
| return title; |
| } |
| |
| return item.url; |
| } |
| } |