diff options
author | Michael Kolb <kolby@google.com> | 2010-09-15 14:55:05 -0700 |
---|---|---|
committer | Michael Kolb <kolby@google.com> | 2010-09-27 14:34:12 -0700 |
commit | 21ce4d295802db811873b46e7821abfa0540dab2 (patch) | |
tree | 38912d62ddc535c7039dd5be01cb846eff7dcb62 /src/com/android/browser/SuggestionsAdapter.java | |
parent | 6ed4dcae944c8188c7f240affeb8c437cbfd2090 (diff) | |
download | packages_apps_browser-21ce4d295802db811873b46e7821abfa0540dab2.zip packages_apps_browser-21ce4d295802db811873b46e7821abfa0540dab2.tar.gz packages_apps_browser-21ce4d295802db811873b46e7821abfa0540dab2.tar.bz2 |
new two column suggestion dropdown
http://b/issue?id=3039704
Change-Id: I8b32553682cc547c695d0089e6633ead77426869
Diffstat (limited to 'src/com/android/browser/SuggestionsAdapter.java')
-rw-r--r-- | src/com/android/browser/SuggestionsAdapter.java | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/src/com/android/browser/SuggestionsAdapter.java b/src/com/android/browser/SuggestionsAdapter.java new file mode 100644 index 0000000..7cfd78e --- /dev/null +++ b/src/com/android/browser/SuggestionsAdapter.java @@ -0,0 +1,591 @@ +/* + * 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 com.android.browser.search.SearchEngine; + +import android.app.SearchManager; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BrowserContract; +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.LinearLayout.LayoutParams; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * adapter to wrap multiple cursors for url/search completions + */ +public class SuggestionsAdapter extends BaseAdapter implements Filterable, OnClickListener { + + static final int TYPE_SEARCH = 0; + static final int TYPE_SUGGEST = 1; + static final int TYPE_BOOKMARK = 2; + static final int TYPE_SUGGEST_URL = 3; + static final int TYPE_HISTORY = 4; + + private static final String[] COMBINED_PROJECTION = + {BrowserContract.Combined._ID, BrowserContract.Combined.TITLE, + BrowserContract.Combined.URL, BrowserContract.Combined.IS_BOOKMARK}; + + private static final String[] SEARCHES_PROJECTION = {BrowserContract.Searches.SEARCH}; + + private static final String COMBINED_SELECTION = + "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)"; + + // Regular expression which matches http://, followed by some stuff, followed by + // optionally a trailing slash, all matched as separate groups. + private static final Pattern STRIP_URL_PATTERN = Pattern.compile("^(http://)(.*?)(/$)?"); + + Context mContext; + Filter mFilter; + SuggestionResults mResults; + List<CursorSource> mSources; + boolean mLandscapeMode; + CompletionListener mListener; + int mLinesPortrait; + int mLinesLandscape; + + interface CompletionListener { + + public void onSearch(String txt); + + public void onSelect(String txt); + + } + + public SuggestionsAdapter(Context ctx, CompletionListener listener) { + mContext = ctx; + 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 SuggestCursor()); + addSource(new SearchesCursor()); + addSource(new CombinedCursor()); + } + + public void setLandscapeMode(boolean mode) { + mLandscapeMode = mode; + } + + public int getLeftCount() { + return mResults.getLeftCount(); + } + + public int getRightCount() { + return mResults.getRightCount(); + } + + public void addSource(CursorSource c) { + if (mSources == null) { + mSources = new ArrayList<CursorSource>(5); + } + mSources.add(c); + } + + @Override + public void onClick(View v) { + if (R.id.icon2 == v.getId()) { + // replace input field text with suggestion text + SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag(); + mListener.onSearch(item.title); + } else { + SuggestItem item = (SuggestItem) v.getTag(); + mListener.onSelect((TextUtils.isEmpty(item.url)? item.title : item.url)); + } + } + + @Override + public Filter getFilter() { + return mFilter; + } + + @Override + public int getCount() { + return (mResults == null) ? 0 : mResults.getLineCount(); + } + + @Override + public SuggestItem getItem(int position) { + if (mResults == null) { + return null; + } + if (mLandscapeMode) { + if (position >= mResults.getLineCount()) { + // right column + position = position - mResults.getLineCount(); + // index in column + if (position >= mResults.getRightCount()) { + return null; + } + return mResults.items.get(position + mResults.getLeftCount()); + } else { + // left column + if (position >= mResults.getLeftCount()) { + return null; + } + return mResults.items.get(position); + } + } else { + return mResults.items.get(position); + } + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final LayoutInflater inflater = LayoutInflater.from(mContext); + if (mLandscapeMode) { + View view = inflater.inflate(R.layout.suggestion_two_column, parent, false); + SuggestItem item = getItem(position); + View iv = view.findViewById(R.id.suggest1); + LayoutParams lp = new LayoutParams(iv.getLayoutParams()); + lp.weight = 0.5f; + iv.setLayoutParams(lp); + if (item != null) { + bindView(iv, item); + } else { + iv.setVisibility((mResults.getLeftCount() == 0) ? View.GONE : + View.INVISIBLE); + } + item = getItem(position + mResults.getLineCount()); + iv = view.findViewById(R.id.suggest2); + lp = new LayoutParams(iv.getLayoutParams()); + lp.weight = 0.5f; + iv.setLayoutParams(lp); + if (item != null) { + bindView(iv, item); + } else { + iv.setVisibility((mResults.getRightCount() == 0) ? View.GONE : + View.INVISIBLE); + } + return view; + } else { + View 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 spacer = view.findViewById(R.id.spacer); + View ic2 = view.findViewById(R.id.icon2); + tv1.setText(item.title); + tv2.setText(item.url); + 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); + spacer.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type)) + ? View.GONE : View.INVISIBLE); + view.setOnClickListener(this); + ic2.setOnClickListener(this); + } + + class SuggestFilter extends Filter { + + int count; + SuggestionResults results; + + @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; + } + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + FilterResults res = new FilterResults(); + if (TextUtils.isEmpty(constraint)) { + res.count = 0; + res.values = null; + return res; + } + results = new SuggestionResults(); + count = 0; + if (constraint != null) { + for (CursorSource sc : mSources) { + sc.runQuery(constraint); + } + mixResults(); + } + res.count = count; + res.values = results; + return res; + } + + void mixResults() { + for (int i = 0; i < mSources.size(); i++) { + CursorSource s = mSources.get(i); + int n = Math.min(s.getCount(), (mLandscapeMode ? mLinesLandscape + : mLinesPortrait)); + boolean more = true; + for (int j = 0; j < n; j++) { + results.addResult(s.getItem()); + more = s.moveToNext(); + } + if (s instanceof SuggestCursor) { + int k = n; + while (more && (k < mLinesPortrait)) { + SuggestItem item = s.getItem(); + if (item.type == TYPE_SUGGEST_URL) { + results.addResult(item); + break; + } + more = s.moveToNext(); + k++; + + } + } + } + + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults fresults) { + mResults = (SuggestionResults) fresults.values; + notifyDataSetChanged(); + } + + } + + /** + * 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() { + if (mLandscapeMode) { + return Math.max(getLeftCount(), getRightCount()); + } else { + return getLeftCount() + getRightCount(); + } + } + + int getLeftCount() { + return counts[TYPE_SEARCH] + counts[TYPE_SUGGEST]; + } + + int getRightCount() { + return counts[TYPE_BOOKMARK] + counts[TYPE_HISTORY] + counts[TYPE_SUGGEST_URL]; + } + + 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 + */ + class SuggestItem { + String title; + String url; + int type; + + 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 = BrowserContract.Combined.CONTENT_URI.buildUpon(); + ub.appendQueryParameter(BrowserContract.PARAM_LIMIT, + Integer.toString(mLinesPortrait)); + mCursor = + mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION, + selection, + (constraint != null) ? args : null, + BrowserContract.Combined.VISITS + " DESC, " + + BrowserContract.Combined.DATE_LAST_VISITED + " DESC"); + 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 = 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) { + return null; + } else { + return stripUrl(url); + } + } + + /** + * Strips the provided url of preceding "http://" and any trailing "/". Does not + * strip "https://". If the provided string cannot be stripped, the original string + * is returned. + * + * TODO: Put this in TextUtils to be used by other packages doing something similar. + * + * @param url a url to strip, like "http://www.google.com/" + * @return a stripped url like "www.google.com", or the original string if it could + * not be stripped + */ + private String stripUrl(String url) { + if (url == null) return null; + Matcher m = STRIP_URL_PATTERN.matcher(url); + if (m.matches() && m.groupCount() == 3) { + return m.group(2); + } else { + return url; + } + } + + } + + class SearchesCursor extends CursorSource { + + @Override + public SuggestItem getItem() { + if ((mCursor != null) && (!mCursor.isAfterLast())) { + return new SuggestItem(mCursor.getString(0), null, TYPE_SEARCH); + } + return null; + } + + @Override + public void runQuery(CharSequence constraint) { + // constraint != null + if (mCursor != null) { + mCursor.close(); + } + String like = constraint + "%"; + String[] args = new String[] {constraint.toString()}; + String selection = BrowserContract.Searches.SEARCH + " LIKE ?"; + Uri.Builder ub = BrowserContract.Searches.CONTENT_URI.buildUpon(); + ub.appendQueryParameter(BrowserContract.PARAM_LIMIT, + Integer.toString(mLinesPortrait)); + mCursor = + mContext.getContentResolver().query(ub.build(), SEARCHES_PROJECTION, + selection, + args, BrowserContract.Searches.DATE + " DESC"); + if (mCursor != null) { + mCursor.moveToFirst(); + } + } + + } + + 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; + return new SuggestItem(title, url, type); + } + return null; + } + + @Override + public void runQuery(CharSequence constraint) { + if (mCursor != null) { + mCursor.close(); + } + if (!TextUtils.isEmpty(constraint)) { + SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine(); + if (searchEngine != null && searchEngine.supportsSuggestions()) { + mCursor = searchEngine.getSuggestions(mContext, constraint.toString()); + if (mCursor != null) { + mCursor.moveToFirst(); + } + } + } else { + mCursor = null; + } + } + + } + +} |