blob: d3609b27496db060f444166c1152649620005ece [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;
25import android.provider.BrowserContract;
26import android.text.TextUtils;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.View.OnClickListener;
30import android.view.ViewGroup;
31import android.widget.BaseAdapter;
32import android.widget.Filter;
33import android.widget.Filterable;
34import android.widget.ImageView;
35import android.widget.LinearLayout.LayoutParams;
36import android.widget.TextView;
37
38import java.util.ArrayList;
39import java.util.List;
40import java.util.regex.Matcher;
41import java.util.regex.Pattern;
42
43/**
44 * adapter to wrap multiple cursors for url/search completions
45 */
46public class SuggestionsAdapter extends BaseAdapter implements Filterable, OnClickListener {
47
48 static final int TYPE_SEARCH = 0;
49 static final int TYPE_SUGGEST = 1;
50 static final int TYPE_BOOKMARK = 2;
51 static final int TYPE_SUGGEST_URL = 3;
52 static final int TYPE_HISTORY = 4;
53
54 private static final String[] COMBINED_PROJECTION =
55 {BrowserContract.Combined._ID, BrowserContract.Combined.TITLE,
56 BrowserContract.Combined.URL, BrowserContract.Combined.IS_BOOKMARK};
57
58 private static final String[] SEARCHES_PROJECTION = {BrowserContract.Searches.SEARCH};
59
60 private static final String COMBINED_SELECTION =
61 "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)";
62
Michael Kolb21ce4d22010-09-15 14:55:05 -070063 Context mContext;
64 Filter mFilter;
65 SuggestionResults mResults;
66 List<CursorSource> mSources;
67 boolean mLandscapeMode;
68 CompletionListener mListener;
69 int mLinesPortrait;
70 int mLinesLandscape;
71
72 interface CompletionListener {
73
74 public void onSearch(String txt);
75
John Reck40f720e2010-11-10 11:57:04 -080076 public void onSelect(String txt, String extraData);
Michael Kolb21ce4d22010-09-15 14:55:05 -070077
Michael Kolb0506f2d2010-10-14 16:20:16 -070078 public void onFilterComplete(int count);
79
Michael Kolb21ce4d22010-09-15 14:55:05 -070080 }
81
82 public SuggestionsAdapter(Context ctx, CompletionListener listener) {
83 mContext = ctx;
84 mListener = listener;
85 mLinesPortrait = mContext.getResources().
86 getInteger(R.integer.max_suggest_lines_portrait);
87 mLinesLandscape = mContext.getResources().
88 getInteger(R.integer.max_suggest_lines_landscape);
89 mFilter = new SuggestFilter();
90 addSource(new SuggestCursor());
91 addSource(new SearchesCursor());
92 addSource(new CombinedCursor());
93 }
94
95 public void setLandscapeMode(boolean mode) {
96 mLandscapeMode = mode;
97 }
98
99 public int getLeftCount() {
100 return mResults.getLeftCount();
101 }
102
103 public int getRightCount() {
104 return mResults.getRightCount();
105 }
106
107 public void addSource(CursorSource c) {
108 if (mSources == null) {
109 mSources = new ArrayList<CursorSource>(5);
110 }
111 mSources.add(c);
112 }
113
114 @Override
115 public void onClick(View v) {
116 if (R.id.icon2 == v.getId()) {
117 // replace input field text with suggestion text
118 SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag();
119 mListener.onSearch(item.title);
120 } else {
121 SuggestItem item = (SuggestItem) v.getTag();
John Reck40f720e2010-11-10 11:57:04 -0800122 mListener.onSelect((TextUtils.isEmpty(item.url)? item.title : item.url),
123 item.extra);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700124 }
125 }
126
127 @Override
128 public Filter getFilter() {
129 return mFilter;
130 }
131
132 @Override
133 public int getCount() {
134 return (mResults == null) ? 0 : mResults.getLineCount();
135 }
136
137 @Override
138 public SuggestItem getItem(int position) {
139 if (mResults == null) {
140 return null;
141 }
142 if (mLandscapeMode) {
143 if (position >= mResults.getLineCount()) {
144 // right column
145 position = position - mResults.getLineCount();
146 // index in column
147 if (position >= mResults.getRightCount()) {
148 return null;
149 }
150 return mResults.items.get(position + mResults.getLeftCount());
151 } else {
152 // left column
153 if (position >= mResults.getLeftCount()) {
154 return null;
155 }
156 return mResults.items.get(position);
157 }
158 } else {
159 return mResults.items.get(position);
160 }
161 }
162
163 @Override
164 public long getItemId(int position) {
165 return 0;
166 }
167
168 @Override
169 public View getView(int position, View convertView, ViewGroup parent) {
170 final LayoutInflater inflater = LayoutInflater.from(mContext);
171 if (mLandscapeMode) {
172 View view = inflater.inflate(R.layout.suggestion_two_column, parent, false);
173 SuggestItem item = getItem(position);
174 View iv = view.findViewById(R.id.suggest1);
175 LayoutParams lp = new LayoutParams(iv.getLayoutParams());
176 lp.weight = 0.5f;
177 iv.setLayoutParams(lp);
178 if (item != null) {
179 bindView(iv, item);
180 } else {
Michael Kolb0506f2d2010-10-14 16:20:16 -0700181 iv.setVisibility((mResults.getLeftCount() == 0) ? View.GONE :
Michael Kolb21ce4d22010-09-15 14:55:05 -0700182 View.INVISIBLE);
183 }
184 item = getItem(position + mResults.getLineCount());
185 iv = view.findViewById(R.id.suggest2);
186 lp = new LayoutParams(iv.getLayoutParams());
187 lp.weight = 0.5f;
188 iv.setLayoutParams(lp);
189 if (item != null) {
190 bindView(iv, item);
191 } else {
Michael Kolb0506f2d2010-10-14 16:20:16 -0700192 iv.setVisibility((mResults.getRightCount() == 0) ? View.GONE :
Michael Kolb21ce4d22010-09-15 14:55:05 -0700193 View.INVISIBLE);
194 }
195 return view;
196 } else {
197 View view = inflater.inflate(R.layout.suggestion_item, parent, false);
198 bindView(view, getItem(position));
199 return view;
200 }
201 }
202
203 private void bindView(View view, SuggestItem item) {
204 // store item for click handling
205 view.setTag(item);
206 TextView tv1 = (TextView) view.findViewById(android.R.id.text1);
207 TextView tv2 = (TextView) view.findViewById(android.R.id.text2);
208 ImageView ic1 = (ImageView) view.findViewById(R.id.icon1);
209 View spacer = view.findViewById(R.id.spacer);
210 View ic2 = view.findViewById(R.id.icon2);
Michael Kolb7b20ddd2010-10-14 15:03:28 -0700211 View div = view.findViewById(R.id.divider);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700212 tv1.setText(item.title);
213 tv2.setText(item.url);
214 int id = -1;
215 switch (item.type) {
216 case TYPE_SUGGEST:
217 case TYPE_SEARCH:
218 id = R.drawable.ic_search_category_suggest;
219 break;
220 case TYPE_BOOKMARK:
221 id = R.drawable.ic_search_category_bookmark;
222 break;
223 case TYPE_HISTORY:
224 id = R.drawable.ic_search_category_history;
225 break;
226 case TYPE_SUGGEST_URL:
227 id = R.drawable.ic_search_category_browser;
228 break;
229 default:
230 id = -1;
231 }
232 if (id != -1) {
233 ic1.setImageDrawable(mContext.getResources().getDrawable(id));
234 }
235 ic2.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type))
236 ? View.VISIBLE : View.GONE);
Michael Kolb7b20ddd2010-10-14 15:03:28 -0700237 div.setVisibility(ic2.getVisibility());
Michael Kolb21ce4d22010-09-15 14:55:05 -0700238 spacer.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type))
239 ? View.GONE : View.INVISIBLE);
240 view.setOnClickListener(this);
241 ic2.setOnClickListener(this);
242 }
243
244 class SuggestFilter extends Filter {
245
Michael Kolb21ce4d22010-09-15 14:55:05 -0700246 SuggestionResults results;
247
248 @Override
249 public CharSequence convertResultToString(Object item) {
250 if (item == null) {
251 return "";
252 }
253 SuggestItem sitem = (SuggestItem) item;
254 if (sitem.title != null) {
255 return sitem.title;
256 } else {
257 return sitem.url;
258 }
259 }
260
261 @Override
262 protected FilterResults performFiltering(CharSequence constraint) {
263 FilterResults res = new FilterResults();
264 if (TextUtils.isEmpty(constraint)) {
265 res.count = 0;
266 res.values = null;
267 return res;
268 }
269 results = new SuggestionResults();
Michael Kolb21ce4d22010-09-15 14:55:05 -0700270 if (constraint != null) {
271 for (CursorSource sc : mSources) {
272 sc.runQuery(constraint);
273 }
274 mixResults();
275 }
Michael Kolb0506f2d2010-10-14 16:20:16 -0700276 res.count = results.getLineCount();
Michael Kolb21ce4d22010-09-15 14:55:05 -0700277 res.values = results;
278 return res;
279 }
280
281 void mixResults() {
282 for (int i = 0; i < mSources.size(); i++) {
283 CursorSource s = mSources.get(i);
Michael Kolb0506f2d2010-10-14 16:20:16 -0700284 int n = Math.min(s.getCount(), (mLandscapeMode ? mLinesLandscape
Michael Kolb21ce4d22010-09-15 14:55:05 -0700285 : mLinesPortrait));
Michael Kolb0506f2d2010-10-14 16:20:16 -0700286 boolean more = false;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700287 for (int j = 0; j < n; j++) {
288 results.addResult(s.getItem());
289 more = s.moveToNext();
290 }
291 if (s instanceof SuggestCursor) {
292 int k = n;
293 while (more && (k < mLinesPortrait)) {
294 SuggestItem item = s.getItem();
295 if (item.type == TYPE_SUGGEST_URL) {
296 results.addResult(item);
297 break;
298 }
299 more = s.moveToNext();
300 k++;
301
302 }
303 }
304 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700305 }
306
307 @Override
308 protected void publishResults(CharSequence constraint, FilterResults fresults) {
309 mResults = (SuggestionResults) fresults.values;
Michael Kolb0506f2d2010-10-14 16:20:16 -0700310 mListener.onFilterComplete(fresults.count);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700311 notifyDataSetChanged();
312 }
313
314 }
315
316 /**
317 * sorted list of results of a suggestion query
318 *
319 */
320 class SuggestionResults {
321
322 ArrayList<SuggestItem> items;
323 // count per type
324 int[] counts;
325
326 SuggestionResults() {
327 items = new ArrayList<SuggestItem>(24);
328 // n of types:
329 counts = new int[5];
330 }
331
332 int getTypeCount(int type) {
333 return counts[type];
334 }
335
336 void addResult(SuggestItem item) {
337 int ix = 0;
338 while ((ix < items.size()) && (item.type >= items.get(ix).type))
339 ix++;
340 items.add(ix, item);
341 counts[item.type]++;
342 }
343
344 int getLineCount() {
345 if (mLandscapeMode) {
346 return Math.max(getLeftCount(), getRightCount());
347 } else {
348 return getLeftCount() + getRightCount();
349 }
350 }
351
352 int getLeftCount() {
353 return counts[TYPE_SEARCH] + counts[TYPE_SUGGEST];
354 }
355
356 int getRightCount() {
357 return counts[TYPE_BOOKMARK] + counts[TYPE_HISTORY] + counts[TYPE_SUGGEST_URL];
358 }
359
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 class SuggestItem {
379 String title;
380 String url;
381 int type;
John Reck40f720e2010-11-10 11:57:04 -0800382 String extra;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700383
384 public SuggestItem(String text, String u, int t) {
385 title = text;
386 url = u;
387 type = t;
388 }
389 }
390
391 abstract class CursorSource {
392
393 Cursor mCursor;
394
395 boolean moveToNext() {
396 return mCursor.moveToNext();
397 }
398
399 public abstract void runQuery(CharSequence constraint);
400
401 public abstract SuggestItem getItem();
402
403 public int getCount() {
404 return (mCursor != null) ? mCursor.getCount() : 0;
405 }
406
407 public void close() {
408 if (mCursor != null) {
409 mCursor.close();
410 }
411 }
412 }
413
414 /**
415 * combined bookmark & history source
416 */
417 class CombinedCursor extends CursorSource {
418
419 @Override
420 public SuggestItem getItem() {
421 if ((mCursor != null) && (!mCursor.isAfterLast())) {
422 String title = mCursor.getString(1);
423 String url = mCursor.getString(2);
424 boolean isBookmark = (mCursor.getInt(3) == 1);
425 return new SuggestItem(getTitle(title, url), getUrl(title, url),
426 isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY);
427 }
428 return null;
429 }
430
431 @Override
432 public void runQuery(CharSequence constraint) {
433 // constraint != null
434 if (mCursor != null) {
435 mCursor.close();
436 }
437 String like = constraint + "%";
438 String[] args = null;
439 String selection = null;
440 if (like.startsWith("http") || like.startsWith("file")) {
441 args = new String[1];
442 args[0] = like;
443 selection = "url LIKE ?";
444 } else {
445 args = new String[5];
446 args[0] = "http://" + like;
447 args[1] = "http://www." + like;
448 args[2] = "https://" + like;
449 args[3] = "https://www." + like;
450 // To match against titles.
451 args[4] = like;
452 selection = COMBINED_SELECTION;
453 }
454 Uri.Builder ub = BrowserContract.Combined.CONTENT_URI.buildUpon();
Michael Kolb0506f2d2010-10-14 16:20:16 -0700455 ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700456 Integer.toString(mLinesPortrait));
457 mCursor =
Michael Kolb0506f2d2010-10-14 16:20:16 -0700458 mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700459 selection,
460 (constraint != null) ? args : null,
461 BrowserContract.Combined.VISITS + " DESC, " +
462 BrowserContract.Combined.DATE_LAST_VISITED + " DESC");
463 if (mCursor != null) {
464 mCursor.moveToFirst();
465 }
466 }
467
468 /**
469 * Provides the title (text line 1) for a browser suggestion, which should be the
470 * webpage title. If the webpage title is empty, returns the stripped url instead.
471 *
472 * @return the title string to use
473 */
474 private String getTitle(String title, String url) {
475 if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
John Reckfb3017f2010-10-26 19:01:24 -0700476 title = UrlUtils.stripUrl(url);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700477 }
478 return title;
479 }
480
481 /**
482 * Provides the subtitle (text line 2) for a browser suggestion, which should be the
483 * webpage url. If the webpage title is empty, then the url should go in the title
484 * instead, and the subtitle should be empty, so this would return null.
485 *
486 * @return the subtitle string to use, or null if none
487 */
488 private String getUrl(String title, String url) {
John Reck7d132b12010-10-26 15:10:21 -0700489 if (TextUtils.isEmpty(title)
490 || TextUtils.getTrimmedLength(title) == 0
491 || title.equals(url)) {
Michael Kolb21ce4d22010-09-15 14:55:05 -0700492 return null;
493 } else {
John Reckfb3017f2010-10-26 19:01:24 -0700494 return UrlUtils.stripUrl(url);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700495 }
496 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700497 }
498
499 class SearchesCursor extends CursorSource {
500
501 @Override
502 public SuggestItem getItem() {
503 if ((mCursor != null) && (!mCursor.isAfterLast())) {
504 return new SuggestItem(mCursor.getString(0), null, TYPE_SEARCH);
505 }
506 return null;
507 }
508
509 @Override
510 public void runQuery(CharSequence constraint) {
511 // constraint != null
512 if (mCursor != null) {
513 mCursor.close();
514 }
515 String like = constraint + "%";
516 String[] args = new String[] {constraint.toString()};
517 String selection = BrowserContract.Searches.SEARCH + " LIKE ?";
518 Uri.Builder ub = BrowserContract.Searches.CONTENT_URI.buildUpon();
Michael Kolb0506f2d2010-10-14 16:20:16 -0700519 ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700520 Integer.toString(mLinesPortrait));
521 mCursor =
Michael Kolb0506f2d2010-10-14 16:20:16 -0700522 mContext.getContentResolver().query(ub.build(), SEARCHES_PROJECTION,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700523 selection,
524 args, BrowserContract.Searches.DATE + " DESC");
525 if (mCursor != null) {
526 mCursor.moveToFirst();
527 }
528 }
529
530 }
531
532 class SuggestCursor extends CursorSource {
533
534 @Override
535 public SuggestItem getItem() {
536 if (mCursor != null) {
537 String title = mCursor.getString(
538 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
539 String text2 = mCursor.getString(
540 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2));
541 String url = mCursor.getString(
542 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL));
543 String uri = mCursor.getString(
544 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA));
545 int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL;
John Reck40f720e2010-11-10 11:57:04 -0800546 SuggestItem item = new SuggestItem(title, url, type);
547 item.extra = mCursor.getString(
548 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA));
549 return item;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700550 }
551 return null;
552 }
553
554 @Override
555 public void runQuery(CharSequence constraint) {
556 if (mCursor != null) {
557 mCursor.close();
558 }
559 if (!TextUtils.isEmpty(constraint)) {
560 SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine();
561 if (searchEngine != null && searchEngine.supportsSuggestions()) {
562 mCursor = searchEngine.getSuggestions(mContext, constraint.toString());
563 if (mCursor != null) {
564 mCursor.moveToFirst();
565 }
566 }
567 } else {
568 mCursor = null;
569 }
570 }
571
572 }
573
574}