blob: 626283a431ecab83a98d4eb41c0b32b65bb76ea0 [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
76 public void onSelect(String txt);
77
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();
122 mListener.onSelect((TextUtils.isEmpty(item.url)? item.title : item.url));
123 }
124 }
125
126 @Override
127 public Filter getFilter() {
128 return mFilter;
129 }
130
131 @Override
132 public int getCount() {
133 return (mResults == null) ? 0 : mResults.getLineCount();
134 }
135
136 @Override
137 public SuggestItem getItem(int position) {
138 if (mResults == null) {
139 return null;
140 }
141 if (mLandscapeMode) {
142 if (position >= mResults.getLineCount()) {
143 // right column
144 position = position - mResults.getLineCount();
145 // index in column
146 if (position >= mResults.getRightCount()) {
147 return null;
148 }
149 return mResults.items.get(position + mResults.getLeftCount());
150 } else {
151 // left column
152 if (position >= mResults.getLeftCount()) {
153 return null;
154 }
155 return mResults.items.get(position);
156 }
157 } else {
158 return mResults.items.get(position);
159 }
160 }
161
162 @Override
163 public long getItemId(int position) {
164 return 0;
165 }
166
167 @Override
168 public View getView(int position, View convertView, ViewGroup parent) {
169 final LayoutInflater inflater = LayoutInflater.from(mContext);
170 if (mLandscapeMode) {
171 View view = inflater.inflate(R.layout.suggestion_two_column, parent, false);
172 SuggestItem item = getItem(position);
173 View iv = view.findViewById(R.id.suggest1);
174 LayoutParams lp = new LayoutParams(iv.getLayoutParams());
175 lp.weight = 0.5f;
176 iv.setLayoutParams(lp);
177 if (item != null) {
178 bindView(iv, item);
179 } else {
Michael Kolb0506f2d2010-10-14 16:20:16 -0700180 iv.setVisibility((mResults.getLeftCount() == 0) ? View.GONE :
Michael Kolb21ce4d22010-09-15 14:55:05 -0700181 View.INVISIBLE);
182 }
183 item = getItem(position + mResults.getLineCount());
184 iv = view.findViewById(R.id.suggest2);
185 lp = new LayoutParams(iv.getLayoutParams());
186 lp.weight = 0.5f;
187 iv.setLayoutParams(lp);
188 if (item != null) {
189 bindView(iv, item);
190 } else {
Michael Kolb0506f2d2010-10-14 16:20:16 -0700191 iv.setVisibility((mResults.getRightCount() == 0) ? View.GONE :
Michael Kolb21ce4d22010-09-15 14:55:05 -0700192 View.INVISIBLE);
193 }
194 return view;
195 } else {
196 View view = inflater.inflate(R.layout.suggestion_item, parent, false);
197 bindView(view, getItem(position));
198 return view;
199 }
200 }
201
202 private void bindView(View view, SuggestItem item) {
203 // store item for click handling
204 view.setTag(item);
205 TextView tv1 = (TextView) view.findViewById(android.R.id.text1);
206 TextView tv2 = (TextView) view.findViewById(android.R.id.text2);
207 ImageView ic1 = (ImageView) view.findViewById(R.id.icon1);
208 View spacer = view.findViewById(R.id.spacer);
209 View ic2 = view.findViewById(R.id.icon2);
Michael Kolb7b20ddd2010-10-14 15:03:28 -0700210 View div = view.findViewById(R.id.divider);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700211 tv1.setText(item.title);
212 tv2.setText(item.url);
213 int id = -1;
214 switch (item.type) {
215 case TYPE_SUGGEST:
216 case TYPE_SEARCH:
217 id = R.drawable.ic_search_category_suggest;
218 break;
219 case TYPE_BOOKMARK:
220 id = R.drawable.ic_search_category_bookmark;
221 break;
222 case TYPE_HISTORY:
223 id = R.drawable.ic_search_category_history;
224 break;
225 case TYPE_SUGGEST_URL:
226 id = R.drawable.ic_search_category_browser;
227 break;
228 default:
229 id = -1;
230 }
231 if (id != -1) {
232 ic1.setImageDrawable(mContext.getResources().getDrawable(id));
233 }
234 ic2.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type))
235 ? View.VISIBLE : View.GONE);
Michael Kolb7b20ddd2010-10-14 15:03:28 -0700236 div.setVisibility(ic2.getVisibility());
Michael Kolb21ce4d22010-09-15 14:55:05 -0700237 spacer.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type))
238 ? View.GONE : View.INVISIBLE);
239 view.setOnClickListener(this);
240 ic2.setOnClickListener(this);
241 }
242
243 class SuggestFilter extends Filter {
244
Michael Kolb21ce4d22010-09-15 14:55:05 -0700245 SuggestionResults results;
246
247 @Override
248 public CharSequence convertResultToString(Object item) {
249 if (item == null) {
250 return "";
251 }
252 SuggestItem sitem = (SuggestItem) item;
253 if (sitem.title != null) {
254 return sitem.title;
255 } else {
256 return sitem.url;
257 }
258 }
259
260 @Override
261 protected FilterResults performFiltering(CharSequence constraint) {
262 FilterResults res = new FilterResults();
263 if (TextUtils.isEmpty(constraint)) {
264 res.count = 0;
265 res.values = null;
266 return res;
267 }
268 results = new SuggestionResults();
Michael Kolb21ce4d22010-09-15 14:55:05 -0700269 if (constraint != null) {
270 for (CursorSource sc : mSources) {
271 sc.runQuery(constraint);
272 }
273 mixResults();
274 }
Michael Kolb0506f2d2010-10-14 16:20:16 -0700275 res.count = results.getLineCount();
Michael Kolb21ce4d22010-09-15 14:55:05 -0700276 res.values = results;
277 return res;
278 }
279
280 void mixResults() {
281 for (int i = 0; i < mSources.size(); i++) {
282 CursorSource s = mSources.get(i);
Michael Kolb0506f2d2010-10-14 16:20:16 -0700283 int n = Math.min(s.getCount(), (mLandscapeMode ? mLinesLandscape
Michael Kolb21ce4d22010-09-15 14:55:05 -0700284 : mLinesPortrait));
Michael Kolb0506f2d2010-10-14 16:20:16 -0700285 boolean more = false;
Michael Kolb21ce4d22010-09-15 14:55:05 -0700286 for (int j = 0; j < n; j++) {
287 results.addResult(s.getItem());
288 more = s.moveToNext();
289 }
290 if (s instanceof SuggestCursor) {
291 int k = n;
292 while (more && (k < mLinesPortrait)) {
293 SuggestItem item = s.getItem();
294 if (item.type == TYPE_SUGGEST_URL) {
295 results.addResult(item);
296 break;
297 }
298 more = s.moveToNext();
299 k++;
300
301 }
302 }
303 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700304 }
305
306 @Override
307 protected void publishResults(CharSequence constraint, FilterResults fresults) {
308 mResults = (SuggestionResults) fresults.values;
Michael Kolb0506f2d2010-10-14 16:20:16 -0700309 mListener.onFilterComplete(fresults.count);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700310 notifyDataSetChanged();
311 }
312
313 }
314
315 /**
316 * sorted list of results of a suggestion query
317 *
318 */
319 class SuggestionResults {
320
321 ArrayList<SuggestItem> items;
322 // count per type
323 int[] counts;
324
325 SuggestionResults() {
326 items = new ArrayList<SuggestItem>(24);
327 // n of types:
328 counts = new int[5];
329 }
330
331 int getTypeCount(int type) {
332 return counts[type];
333 }
334
335 void addResult(SuggestItem item) {
336 int ix = 0;
337 while ((ix < items.size()) && (item.type >= items.get(ix).type))
338 ix++;
339 items.add(ix, item);
340 counts[item.type]++;
341 }
342
343 int getLineCount() {
344 if (mLandscapeMode) {
345 return Math.max(getLeftCount(), getRightCount());
346 } else {
347 return getLeftCount() + getRightCount();
348 }
349 }
350
351 int getLeftCount() {
352 return counts[TYPE_SEARCH] + counts[TYPE_SUGGEST];
353 }
354
355 int getRightCount() {
356 return counts[TYPE_BOOKMARK] + counts[TYPE_HISTORY] + counts[TYPE_SUGGEST_URL];
357 }
358
359 public String toString() {
360 if (items == null) return null;
361 if (items.size() == 0) return "[]";
362 StringBuilder sb = new StringBuilder();
363 for (int i = 0; i < items.size(); i++) {
364 SuggestItem item = items.get(i);
365 sb.append(item.type + ": " + item.title);
366 if (i < items.size() - 1) {
367 sb.append(", ");
368 }
369 }
370 return sb.toString();
371 }
372 }
373
374 /**
375 * data object to hold suggestion values
376 */
377 class SuggestItem {
378 String title;
379 String url;
380 int type;
381
382 public SuggestItem(String text, String u, int t) {
383 title = text;
384 url = u;
385 type = t;
386 }
387 }
388
389 abstract class CursorSource {
390
391 Cursor mCursor;
392
393 boolean moveToNext() {
394 return mCursor.moveToNext();
395 }
396
397 public abstract void runQuery(CharSequence constraint);
398
399 public abstract SuggestItem getItem();
400
401 public int getCount() {
402 return (mCursor != null) ? mCursor.getCount() : 0;
403 }
404
405 public void close() {
406 if (mCursor != null) {
407 mCursor.close();
408 }
409 }
410 }
411
412 /**
413 * combined bookmark & history source
414 */
415 class CombinedCursor extends CursorSource {
416
417 @Override
418 public SuggestItem getItem() {
419 if ((mCursor != null) && (!mCursor.isAfterLast())) {
420 String title = mCursor.getString(1);
421 String url = mCursor.getString(2);
422 boolean isBookmark = (mCursor.getInt(3) == 1);
423 return new SuggestItem(getTitle(title, url), getUrl(title, url),
424 isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY);
425 }
426 return null;
427 }
428
429 @Override
430 public void runQuery(CharSequence constraint) {
431 // constraint != null
432 if (mCursor != null) {
433 mCursor.close();
434 }
435 String like = constraint + "%";
436 String[] args = null;
437 String selection = null;
438 if (like.startsWith("http") || like.startsWith("file")) {
439 args = new String[1];
440 args[0] = like;
441 selection = "url LIKE ?";
442 } else {
443 args = new String[5];
444 args[0] = "http://" + like;
445 args[1] = "http://www." + like;
446 args[2] = "https://" + like;
447 args[3] = "https://www." + like;
448 // To match against titles.
449 args[4] = like;
450 selection = COMBINED_SELECTION;
451 }
452 Uri.Builder ub = BrowserContract.Combined.CONTENT_URI.buildUpon();
Michael Kolb0506f2d2010-10-14 16:20:16 -0700453 ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700454 Integer.toString(mLinesPortrait));
455 mCursor =
Michael Kolb0506f2d2010-10-14 16:20:16 -0700456 mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700457 selection,
458 (constraint != null) ? args : null,
459 BrowserContract.Combined.VISITS + " DESC, " +
460 BrowserContract.Combined.DATE_LAST_VISITED + " DESC");
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) {
John Reckfb3017f2010-10-26 19:01:24 -0700474 title = UrlUtils.stripUrl(url);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700475 }
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) {
John Reck7d132b12010-10-26 15:10:21 -0700487 if (TextUtils.isEmpty(title)
488 || TextUtils.getTrimmedLength(title) == 0
489 || title.equals(url)) {
Michael Kolb21ce4d22010-09-15 14:55:05 -0700490 return null;
491 } else {
John Reckfb3017f2010-10-26 19:01:24 -0700492 return UrlUtils.stripUrl(url);
Michael Kolb21ce4d22010-09-15 14:55:05 -0700493 }
494 }
Michael Kolb21ce4d22010-09-15 14:55:05 -0700495 }
496
497 class SearchesCursor extends CursorSource {
498
499 @Override
500 public SuggestItem getItem() {
501 if ((mCursor != null) && (!mCursor.isAfterLast())) {
502 return new SuggestItem(mCursor.getString(0), null, TYPE_SEARCH);
503 }
504 return null;
505 }
506
507 @Override
508 public void runQuery(CharSequence constraint) {
509 // constraint != null
510 if (mCursor != null) {
511 mCursor.close();
512 }
513 String like = constraint + "%";
514 String[] args = new String[] {constraint.toString()};
515 String selection = BrowserContract.Searches.SEARCH + " LIKE ?";
516 Uri.Builder ub = BrowserContract.Searches.CONTENT_URI.buildUpon();
Michael Kolb0506f2d2010-10-14 16:20:16 -0700517 ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700518 Integer.toString(mLinesPortrait));
519 mCursor =
Michael Kolb0506f2d2010-10-14 16:20:16 -0700520 mContext.getContentResolver().query(ub.build(), SEARCHES_PROJECTION,
Michael Kolb21ce4d22010-09-15 14:55:05 -0700521 selection,
522 args, BrowserContract.Searches.DATE + " DESC");
523 if (mCursor != null) {
524 mCursor.moveToFirst();
525 }
526 }
527
528 }
529
530 class SuggestCursor extends CursorSource {
531
532 @Override
533 public SuggestItem getItem() {
534 if (mCursor != null) {
535 String title = mCursor.getString(
536 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
537 String text2 = mCursor.getString(
538 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2));
539 String url = mCursor.getString(
540 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL));
541 String uri = mCursor.getString(
542 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA));
543 int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL;
544 return new SuggestItem(title, url, type);
545 }
546 return null;
547 }
548
549 @Override
550 public void runQuery(CharSequence constraint) {
551 if (mCursor != null) {
552 mCursor.close();
553 }
554 if (!TextUtils.isEmpty(constraint)) {
555 SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine();
556 if (searchEngine != null && searchEngine.supportsSuggestions()) {
557 mCursor = searchEngine.getSuggestions(mContext, constraint.toString());
558 if (mCursor != null) {
559 mCursor.moveToFirst();
560 }
561 }
562 } else {
563 mCursor = null;
564 }
565 }
566
567 }
568
569}