blob: 7ee5c2a04b2761aa091e98abd02b804ac11777c6 [file] [log] [blame]
Michael Kolb21ce4d22010-09-15 14:55:05 -07001/*
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
17package com.android.browser;
18
19import com.android.browser.search.SearchEngine;
20
21import android.app.SearchManager;
22import android.content.Context;
23import android.database.Cursor;
24import android.net.Uri;
John Reck35defff2010-11-11 14:06:45 -080025import android.os.AsyncTask;
Michael Kolb21ce4d22010-09-15 14:55:05 -070026import android.provider.BrowserContract;
27import android.text.TextUtils;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.View.OnClickListener;
31import android.view.ViewGroup;
32import android.widget.BaseAdapter;
33import android.widget.Filter;
34import android.widget.Filterable;
35import android.widget.ImageView;
Michael Kolb21ce4d22010-09-15 14:55:05 -070036import android.widget.TextView;
37
38import java.util.ArrayList;
39import java.util.List;
Michael Kolb21ce4d22010-09-15 14:55:05 -070040
41/**
42 * adapter to wrap multiple cursors for url/search completions
43 */
44public class SuggestionsAdapter extends BaseAdapter implements Filterable, OnClickListener {
45
John Reck35defff2010-11-11 14:06:45 -080046 static final int TYPE_BOOKMARK = 0;
47 static final int TYPE_SUGGEST_URL = 1;
48 static final int TYPE_HISTORY = 2;
49 static final int TYPE_SEARCH = 3;
50 static final int TYPE_SUGGEST = 4;
Michael Kolb21ce4d22010-09-15 14:55:05 -070051
52 private static final String[] COMBINED_PROJECTION =
53 {BrowserContract.Combined._ID, BrowserContract.Combined.TITLE,
54 BrowserContract.Combined.URL, BrowserContract.Combined.IS_BOOKMARK};
55
Michael Kolb21ce4d22010-09-15 14:55:05 -070056 private static final String COMBINED_SELECTION =
57 "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)";
58
Michael Kolb21ce4d22010-09-15 14:55:05 -070059 Context mContext;
60 Filter mFilter;
John Reck35defff2010-11-11 14:06:45 -080061 SuggestionResults mMixedResults;
62 List<SuggestItem> mSuggestResults, mFilterResults;
Michael Kolb21ce4d22010-09-15 14:55:05 -070063 List<CursorSource> mSources;
64 boolean mLandscapeMode;
65 CompletionListener mListener;
66 int mLinesPortrait;
67 int mLinesLandscape;
John Reck35defff2010-11-11 14:06:45 -080068 Object mResultsLock = new Object();
Michael Kolbcfa3af52010-12-14 10:36:11 -080069 List<String> mVoiceResults;
Michael Kolb21ce4d22010-09-15 14:55:05 -070070
71 interface CompletionListener {
72
73 public void onSearch(String txt);
74
John Reck40f720e2010-11-10 11:57:04 -080075 public void onSelect(String txt, String extraData);
Michael Kolb21ce4d22010-09-15 14:55:05 -070076
Michael Kolb0506f2d2010-10-14 16:20:16 -070077 public void onFilterComplete(int count);
78
Michael Kolb21ce4d22010-09-15 14:55:05 -070079 }
80
81 public SuggestionsAdapter(Context ctx, CompletionListener listener) {
82 mContext = ctx;
83 mListener = listener;
84 mLinesPortrait = mContext.getResources().
85 getInteger(R.integer.max_suggest_lines_portrait);
86 mLinesLandscape = mContext.getResources().
87 getInteger(R.integer.max_suggest_lines_landscape);
88 mFilter = new SuggestFilter();
Michael Kolb21ce4d22010-09-15 14:55:05 -070089 addSource(new CombinedCursor());
90 }
91
Michael Kolbcfa3af52010-12-14 10:36:11 -080092 void setVoiceResults(List<String> voiceResults) {
93 mVoiceResults = voiceResults;
Michael Kolba50c4462010-12-15 10:49:12 -080094 notifyDataSetChanged();
Michael Kolbcfa3af52010-12-14 10:36:11 -080095 }
96
Michael Kolb21ce4d22010-09-15 14:55:05 -070097 public void setLandscapeMode(boolean mode) {
98 mLandscapeMode = mode;
John Reck35defff2010-11-11 14:06:45 -080099 notifyDataSetChanged();
Michael Kolb21ce4d22010-09-15 14:55:05 -0700100 }
101
Michael Kolb21ce4d22010-09-15 14:55:05 -0700102 public void addSource(CursorSource c) {
103 if (mSources == null) {
104 mSources = new ArrayList<CursorSource>(5);
105 }
106 mSources.add(c);
107 }
108
109 @Override
110 public void onClick(View v) {
John Reckad373302010-12-17 15:28:13 -0800111 SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag();
Michael Kolb21ce4d22010-09-15 14:55:05 -0700112 if (R.id.icon2 == v.getId()) {
113 // replace input field text with suggestion text
Michael Kolb21ce4d22010-09-15 14:55:05 -0700114 mListener.onSearch(item.title);
115 } else {
John Reck40f720e2010-11-10 11:57:04 -0800116 mListener.onSelect((TextUtils.isEmpty(item.url)? item.title : item.url),
117 item.extra);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700118 }
119 }
120
121 @Override
122 public Filter getFilter() {
123 return mFilter;
124 }
125
126 @Override
127 public int getCount() {
Michael Kolbcfa3af52010-12-14 10:36:11 -0800128 if (mVoiceResults != null) {
129 return mVoiceResults.size();
130 }
John Reck35defff2010-11-11 14:06:45 -0800131 return (mMixedResults == null) ? 0 : mMixedResults.getLineCount();
Michael Kolb21ce4d22010-09-15 14:55:05 -0700132 }
133
134 @Override
135 public SuggestItem getItem(int position) {
Michael Kolbcfa3af52010-12-14 10:36:11 -0800136 if (mVoiceResults != null) {
137 return new SuggestItem(mVoiceResults.get(position), null,
138 TYPE_SEARCH);
139 }
John Reck35defff2010-11-11 14:06:45 -0800140 if (mMixedResults == null) {
Michael Kolb21ce4d22010-09-15 14:55:05 -0700141 return null;
142 }
John Reckad373302010-12-17 15:28:13 -0800143 return mMixedResults.items.get(position);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700144 }
145
146 @Override
147 public long getItemId(int position) {
148 return 0;
149 }
150
151 @Override
152 public View getView(int position, View convertView, ViewGroup parent) {
153 final LayoutInflater inflater = LayoutInflater.from(mContext);
John Reck35defff2010-11-11 14:06:45 -0800154 View view = convertView;
155 if (view == null) {
John Reckad373302010-12-17 15:28:13 -0800156 view = inflater.inflate(R.layout.suggestion_item, parent, false);
John Reck35defff2010-11-11 14:06:45 -0800157 }
John Reckad373302010-12-17 15:28:13 -0800158 bindView(view, getItem(position));
159 return view;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700160 }
161
162 private void bindView(View view, SuggestItem item) {
163 // store item for click handling
164 view.setTag(item);
165 TextView tv1 = (TextView) view.findViewById(android.R.id.text1);
166 TextView tv2 = (TextView) view.findViewById(android.R.id.text2);
167 ImageView ic1 = (ImageView) view.findViewById(R.id.icon1);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700168 View ic2 = view.findViewById(R.id.icon2);
Michael Kolb7b20ddd2010-10-14 15:03:28 -0700169 View div = view.findViewById(R.id.divider);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700170 tv1.setText(item.title);
John Reckad373302010-12-17 15:28:13 -0800171 if (TextUtils.isEmpty(item.url)) {
172 tv2.setVisibility(View.GONE);
173 } else {
174 tv2.setVisibility(View.VISIBLE);
175 tv2.setText(item.url);
176 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700177 int id = -1;
178 switch (item.type) {
179 case TYPE_SUGGEST:
180 case TYPE_SEARCH:
181 id = R.drawable.ic_search_category_suggest;
182 break;
183 case TYPE_BOOKMARK:
184 id = R.drawable.ic_search_category_bookmark;
185 break;
186 case TYPE_HISTORY:
187 id = R.drawable.ic_search_category_history;
188 break;
189 case TYPE_SUGGEST_URL:
190 id = R.drawable.ic_search_category_browser;
191 break;
192 default:
193 id = -1;
194 }
195 if (id != -1) {
196 ic1.setImageDrawable(mContext.getResources().getDrawable(id));
197 }
198 ic2.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type))
199 ? View.VISIBLE : View.GONE);
Michael Kolb7b20ddd2010-10-14 15:03:28 -0700200 div.setVisibility(ic2.getVisibility());
Michael Kolb21ce4d22010-09-15 14:55:05 -0700201 ic2.setOnClickListener(this);
John Reckad373302010-12-17 15:28:13 -0800202 view.findViewById(R.id.suggestion).setOnClickListener(this);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700203 }
204
John Reck35defff2010-11-11 14:06:45 -0800205 class SlowFilterTask extends AsyncTask<CharSequence, Void, List<SuggestItem>> {
Michael Kolb21ce4d22010-09-15 14:55:05 -0700206
John Reck35defff2010-11-11 14:06:45 -0800207 @Override
208 protected List<SuggestItem> doInBackground(CharSequence... params) {
209 SuggestCursor cursor = new SuggestCursor();
210 cursor.runQuery(params[0]);
211 List<SuggestItem> results = new ArrayList<SuggestItem>();
212 int count = cursor.getCount();
213 for (int i = 0; i < count; i++) {
214 results.add(cursor.getItem());
215 cursor.moveToNext();
216 }
217 cursor.close();
218 return results;
219 }
220
221 @Override
222 protected void onPostExecute(List<SuggestItem> items) {
223 mSuggestResults = items;
224 mMixedResults = buildSuggestionResults();
225 notifyDataSetChanged();
226 mListener.onFilterComplete(mMixedResults.getLineCount());
227 }
228 }
229
230 SuggestionResults buildSuggestionResults() {
231 SuggestionResults mixed = new SuggestionResults();
232 List<SuggestItem> filter, suggest;
233 synchronized (mResultsLock) {
234 filter = mFilterResults;
235 suggest = mSuggestResults;
236 }
237 if (filter != null) {
238 for (SuggestItem item : filter) {
239 mixed.addResult(item);
240 }
241 }
242 if (suggest != null) {
243 for (SuggestItem item : suggest) {
244 mixed.addResult(item);
245 }
246 }
247 return mixed;
248 }
249
250 class SuggestFilter extends Filter {
Michael Kolb21ce4d22010-09-15 14:55:05 -0700251
252 @Override
253 public CharSequence convertResultToString(Object item) {
254 if (item == null) {
255 return "";
256 }
257 SuggestItem sitem = (SuggestItem) item;
258 if (sitem.title != null) {
259 return sitem.title;
260 } else {
261 return sitem.url;
262 }
263 }
264
John Reck35defff2010-11-11 14:06:45 -0800265 void startSuggestionsAsync(final CharSequence constraint) {
266 new SlowFilterTask().execute(constraint);
267 }
268
Michael Kolb21ce4d22010-09-15 14:55:05 -0700269 @Override
270 protected FilterResults performFiltering(CharSequence constraint) {
271 FilterResults res = new FilterResults();
Michael Kolbcfa3af52010-12-14 10:36:11 -0800272 if (mVoiceResults == null) {
273 if (TextUtils.isEmpty(constraint)) {
274 res.count = 0;
275 res.values = null;
276 return res;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700277 }
Michael Kolbcfa3af52010-12-14 10:36:11 -0800278 startSuggestionsAsync(constraint);
279 List<SuggestItem> filterResults = new ArrayList<SuggestItem>();
280 if (constraint != null) {
281 for (CursorSource sc : mSources) {
282 sc.runQuery(constraint);
283 }
284 mixResults(filterResults);
285 }
286 synchronized (mResultsLock) {
287 mFilterResults = filterResults;
288 }
289 SuggestionResults mixed = buildSuggestionResults();
290 res.count = mixed.getLineCount();
291 res.values = mixed;
292 } else {
293 res.count = mVoiceResults.size();
294 res.values = mVoiceResults;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700295 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700296 return res;
297 }
298
John Reck35defff2010-11-11 14:06:45 -0800299 void mixResults(List<SuggestItem> results) {
John Reckad373302010-12-17 15:28:13 -0800300 int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait;
301 maxLines = (int) Math.ceil(maxLines / 2.0);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700302 for (int i = 0; i < mSources.size(); i++) {
303 CursorSource s = mSources.get(i);
John Reck35defff2010-11-11 14:06:45 -0800304 int n = Math.min(s.getCount(), maxLines);
305 maxLines -= n;
Michael Kolb0506f2d2010-10-14 16:20:16 -0700306 boolean more = false;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700307 for (int j = 0; j < n; j++) {
John Reck35defff2010-11-11 14:06:45 -0800308 results.add(s.getItem());
Michael Kolb21ce4d22010-09-15 14:55:05 -0700309 more = s.moveToNext();
310 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700311 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700312 }
313
314 @Override
315 protected void publishResults(CharSequence constraint, FilterResults fresults) {
Michael Kolbcfa3af52010-12-14 10:36:11 -0800316 if (fresults.values instanceof SuggestionResults) {
317 mMixedResults = (SuggestionResults) fresults.values;
318 mListener.onFilterComplete(fresults.count);
319 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700320 notifyDataSetChanged();
321 }
322
323 }
324
325 /**
326 * sorted list of results of a suggestion query
327 *
328 */
329 class SuggestionResults {
330
331 ArrayList<SuggestItem> items;
332 // count per type
333 int[] counts;
334
335 SuggestionResults() {
336 items = new ArrayList<SuggestItem>(24);
337 // n of types:
338 counts = new int[5];
339 }
340
341 int getTypeCount(int type) {
342 return counts[type];
343 }
344
345 void addResult(SuggestItem item) {
346 int ix = 0;
347 while ((ix < items.size()) && (item.type >= items.get(ix).type))
348 ix++;
349 items.add(ix, item);
350 counts[item.type]++;
351 }
352
353 int getLineCount() {
354 if (mLandscapeMode) {
John Reckad373302010-12-17 15:28:13 -0800355 return Math.min(mLinesLandscape, items.size());
Michael Kolb21ce4d22010-09-15 14:55:05 -0700356 } else {
John Reckad373302010-12-17 15:28:13 -0800357 return Math.min(mLinesPortrait, items.size());
Michael Kolb21ce4d22010-09-15 14:55:05 -0700358 }
359 }
360
John Reck35defff2010-11-11 14:06:45 -0800361 @Override
Michael Kolb21ce4d22010-09-15 14:55:05 -0700362 public String toString() {
363 if (items == null) return null;
364 if (items.size() == 0) return "[]";
365 StringBuilder sb = new StringBuilder();
366 for (int i = 0; i < items.size(); i++) {
367 SuggestItem item = items.get(i);
368 sb.append(item.type + ": " + item.title);
369 if (i < items.size() - 1) {
370 sb.append(", ");
371 }
372 }
373 return sb.toString();
374 }
375 }
376
377 /**
378 * data object to hold suggestion values
379 */
380 class SuggestItem {
381 String title;
382 String url;
383 int type;
John Reck40f720e2010-11-10 11:57:04 -0800384 String extra;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700385
386 public SuggestItem(String text, String u, int t) {
387 title = text;
388 url = u;
389 type = t;
390 }
391 }
392
393 abstract class CursorSource {
394
395 Cursor mCursor;
396
397 boolean moveToNext() {
398 return mCursor.moveToNext();
399 }
400
401 public abstract void runQuery(CharSequence constraint);
402
403 public abstract SuggestItem getItem();
404
405 public int getCount() {
406 return (mCursor != null) ? mCursor.getCount() : 0;
407 }
408
409 public void close() {
410 if (mCursor != null) {
411 mCursor.close();
412 }
413 }
414 }
415
416 /**
417 * combined bookmark & history source
418 */
419 class CombinedCursor extends CursorSource {
420
421 @Override
422 public SuggestItem getItem() {
423 if ((mCursor != null) && (!mCursor.isAfterLast())) {
424 String title = mCursor.getString(1);
425 String url = mCursor.getString(2);
426 boolean isBookmark = (mCursor.getInt(3) == 1);
427 return new SuggestItem(getTitle(title, url), getUrl(title, url),
428 isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY);
429 }
430 return null;
431 }
432
433 @Override
434 public void runQuery(CharSequence constraint) {
435 // constraint != null
436 if (mCursor != null) {
437 mCursor.close();
438 }
439 String like = constraint + "%";
440 String[] args = null;
441 String selection = null;
442 if (like.startsWith("http") || like.startsWith("file")) {
443 args = new String[1];
444 args[0] = like;
445 selection = "url LIKE ?";
446 } else {
447 args = new String[5];
448 args[0] = "http://" + like;
449 args[1] = "http://www." + like;
450 args[2] = "https://" + like;
451 args[3] = "https://www." + like;
452 // To match against titles.
453 args[4] = like;
454 selection = COMBINED_SELECTION;
455 }
456 Uri.Builder ub = BrowserContract.Combined.CONTENT_URI.buildUpon();
Michael Kolb0506f2d2010-10-14 16:20:16 -0700457 ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
John Reckad373302010-12-17 15:28:13 -0800458 Integer.toString(Math.max(mLinesLandscape, mLinesPortrait)));
John Reck3dff1ce2010-12-10 11:13:57 -0800459 BookmarkUtils.addAccountInfo(mContext, ub);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700460 mCursor =
Michael Kolb0506f2d2010-10-14 16:20:16 -0700461 mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700462 selection,
463 (constraint != null) ? args : null,
John Reck3dff1ce2010-12-10 11:13:57 -0800464 BrowserContract.Combined.IS_BOOKMARK + " DESC, " +
Michael Kolb21ce4d22010-09-15 14:55:05 -0700465 BrowserContract.Combined.VISITS + " DESC, " +
466 BrowserContract.Combined.DATE_LAST_VISITED + " DESC");
467 if (mCursor != null) {
468 mCursor.moveToFirst();
469 }
470 }
471
472 /**
473 * Provides the title (text line 1) for a browser suggestion, which should be the
474 * webpage title. If the webpage title is empty, returns the stripped url instead.
475 *
476 * @return the title string to use
477 */
478 private String getTitle(String title, String url) {
479 if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
John Reckfb3017f2010-10-26 19:01:24 -0700480 title = UrlUtils.stripUrl(url);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700481 }
482 return title;
483 }
484
485 /**
486 * Provides the subtitle (text line 2) for a browser suggestion, which should be the
487 * webpage url. If the webpage title is empty, then the url should go in the title
488 * instead, and the subtitle should be empty, so this would return null.
489 *
490 * @return the subtitle string to use, or null if none
491 */
492 private String getUrl(String title, String url) {
John Reck7d132b12010-10-26 15:10:21 -0700493 if (TextUtils.isEmpty(title)
494 || TextUtils.getTrimmedLength(title) == 0
495 || title.equals(url)) {
Michael Kolb21ce4d22010-09-15 14:55:05 -0700496 return null;
497 } else {
John Reckfb3017f2010-10-26 19:01:24 -0700498 return UrlUtils.stripUrl(url);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700499 }
500 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700501 }
502
Michael Kolb21ce4d22010-09-15 14:55:05 -0700503 class SuggestCursor extends CursorSource {
504
505 @Override
506 public SuggestItem getItem() {
507 if (mCursor != null) {
508 String title = mCursor.getString(
509 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
510 String text2 = mCursor.getString(
511 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2));
512 String url = mCursor.getString(
513 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL));
514 String uri = mCursor.getString(
515 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA));
516 int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL;
John Reck40f720e2010-11-10 11:57:04 -0800517 SuggestItem item = new SuggestItem(title, url, type);
518 item.extra = mCursor.getString(
519 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA));
520 return item;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700521 }
522 return null;
523 }
524
525 @Override
526 public void runQuery(CharSequence constraint) {
527 if (mCursor != null) {
528 mCursor.close();
529 }
530 if (!TextUtils.isEmpty(constraint)) {
531 SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine();
532 if (searchEngine != null && searchEngine.supportsSuggestions()) {
533 mCursor = searchEngine.getSuggestions(mContext, constraint.toString());
534 if (mCursor != null) {
535 mCursor.moveToFirst();
536 }
537 }
538 } else {
539 mCursor = null;
540 }
541 }
542
543 }
544
John Reck35defff2010-11-11 14:06:45 -0800545 public void clearCache() {
546 mFilterResults = null;
547 mSuggestResults = null;
548 }
549
Michael Kolb21ce4d22010-09-15 14:55:05 -0700550}