1 /* 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 17 package com.android.browser; 18 19 import android.app.SearchManager; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.net.Uri; 23 import android.os.AsyncTask; 24 import android.provider.BrowserContract; 25 import android.text.Html; 26 import android.text.TextUtils; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.View.OnClickListener; 30 import android.view.ViewGroup; 31 import android.widget.BaseAdapter; 32 import android.widget.Filter; 33 import android.widget.Filterable; 34 import android.widget.ImageView; 35 import android.widget.TextView; 36 37 import com.android.browser.provider.BrowserProvider2.OmniboxSuggestions; 38 import com.android.browser.search.SearchEngine; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * adapter to wrap multiple cursors for url/search completions 45 */ 46 public class SuggestionsAdapter extends BaseAdapter implements Filterable, 47 OnClickListener { 48 49 public static final int TYPE_BOOKMARK = 0; 50 public static final int TYPE_HISTORY = 1; 51 public static final int TYPE_SUGGEST_URL = 2; 52 public static final int TYPE_SEARCH = 3; 53 public static final int TYPE_SUGGEST = 4; 54 55 private static final String[] COMBINED_PROJECTION = { 56 OmniboxSuggestions._ID, 57 OmniboxSuggestions.TITLE, 58 OmniboxSuggestions.URL, 59 OmniboxSuggestions.IS_BOOKMARK 60 }; 61 62 private static final String COMBINED_SELECTION = 63 "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)"; 64 65 final Context mContext; 66 final Filter mFilter; 67 SuggestionResults mMixedResults; 68 List<SuggestItem> mSuggestResults, mFilterResults; 69 List<CursorSource> mSources; 70 boolean mLandscapeMode; 71 final CompletionListener mListener; 72 final int mLinesPortrait; 73 final int mLinesLandscape; 74 final Object mResultsLock = new Object(); 75 boolean mIncognitoMode; 76 BrowserSettings mSettings; 77 78 interface CompletionListener { 79 80 public void onSearch(String txt); 81 82 public void onSelect(String txt, int type, String extraData); 83 84 } 85 86 public SuggestionsAdapter(Context ctx, CompletionListener listener) { 87 mContext = ctx; 88 mSettings = BrowserSettings.getInstance(); 89 mListener = listener; 90 mLinesPortrait = mContext.getResources(). 91 getInteger(R.integer.max_suggest_lines_portrait); 92 mLinesLandscape = mContext.getResources(). 93 getInteger(R.integer.max_suggest_lines_landscape); 94 95 mFilter = new SuggestFilter(); 96 addSource(new CombinedCursor()); 97 } 98 99 public void setLandscapeMode(boolean mode) { 100 mLandscapeMode = mode; 101 notifyDataSetChanged(); 102 } 103 104 public void addSource(CursorSource c) { 105 if (mSources == null) { 106 mSources = new ArrayList<CursorSource>(5); 107 } 108 mSources.add(c); 109 } 110 111 @Override 112 public void onClick(View v) { 113 SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag(); 114 115 if (R.id.icon2 == v.getId()) { 116 // replace input field text with suggestion text 117 mListener.onSearch(getSuggestionUrl(item)); 118 } else { 119 mListener.onSelect(getSuggestionUrl(item), item.type, item.extra); 120 } 121 } 122 123 @Override 124 public Filter getFilter() { 125 return mFilter; 126 } 127 128 @Override 129 public int getCount() { 130 return (mMixedResults == null) ? 0 : mMixedResults.getLineCount(); 131 } 132 133 @Override 134 public SuggestItem getItem(int position) { 135 if (mMixedResults == null) { 136 return null; 137 } 138 return mMixedResults.items.get(position); 139 } 140 141 @Override 142 public long getItemId(int position) { 143 return position; 144 } 145 146 @Override 147 public View getView(int position, View convertView, ViewGroup parent) { 148 final LayoutInflater inflater = LayoutInflater.from(mContext); 149 View view = convertView; 150 if (view == null) { 151 view = inflater.inflate(R.layout.suggestion_item, parent, false); 152 } 153 bindView(view, getItem(position)); 154 return view; 155 } 156 157 private void bindView(View view, SuggestItem item) { 158 // store item for click handling 159 view.setTag(item); 160 TextView tv1 = (TextView) view.findViewById(android.R.id.text1); 161 TextView tv2 = (TextView) view.findViewById(android.R.id.text2); 162 ImageView ic1 = (ImageView) view.findViewById(R.id.icon1); 163 View ic2 = view.findViewById(R.id.icon2); 164 View div = view.findViewById(R.id.divider); 165 tv1.setText(Html.fromHtml(item.title)); 166 if (TextUtils.isEmpty(item.url)) { 167 tv2.setVisibility(View.GONE); 168 tv1.setMaxLines(2); 169 } else { 170 tv2.setVisibility(View.VISIBLE); 171 tv2.setText(item.url); 172 tv1.setMaxLines(1); 173 } 174 int id = -1; 175 switch (item.type) { 176 case TYPE_SUGGEST: 177 case TYPE_SEARCH: 178 id = R.drawable.ic_search_category_suggest; 179 break; 180 case TYPE_BOOKMARK: 181 id = R.drawable.ic_search_category_bookmark; 182 break; 183 case TYPE_HISTORY: 184 id = R.drawable.ic_search_category_history; 185 break; 186 case TYPE_SUGGEST_URL: 187 id = R.drawable.ic_search_category_browser; 188 break; 189 default: 190 id = -1; 191 } 192 if (id != -1) { 193 ic1.setImageDrawable(mContext.getResources().getDrawable(id)); 194 } 195 ic2.setVisibility(((TYPE_SUGGEST == item.type) 196 || (TYPE_SEARCH == item.type)) 197 ? View.VISIBLE : View.GONE); 198 div.setVisibility(ic2.getVisibility()); 199 ic2.setOnClickListener(this); 200 view.findViewById(R.id.suggestion).setOnClickListener(this); 201 } 202 203 class SlowFilterTask extends AsyncTask<CharSequence, Void, List<SuggestItem>> { 204 205 @Override 206 protected List<SuggestItem> doInBackground(CharSequence... params) { 207 SuggestCursor cursor = new SuggestCursor(); 208 cursor.runQuery(params[0]); 209 List<SuggestItem> results = new ArrayList<SuggestItem>(); 210 int count = cursor.getCount(); 211 for (int i = 0; i < count; i++) { 212 results.add(cursor.getItem()); 213 cursor.moveToNext(); 214 } 215 cursor.close(); 216 return results; 217 } 218 219 @Override 220 protected void onPostExecute(List<SuggestItem> items) { 221 mSuggestResults = items; 222 mMixedResults = buildSuggestionResults(); 223 notifyDataSetChanged(); 224 } 225 } 226 227 SuggestionResults buildSuggestionResults() { 228 SuggestionResults mixed = new SuggestionResults(); 229 List<SuggestItem> filter, suggest; 230 synchronized (mResultsLock) { 231 filter = mFilterResults; 232 suggest = mSuggestResults; 233 } 234 if (filter != null) { 235 for (SuggestItem item : filter) { 236 mixed.addResult(item); 237 } 238 } 239 if (suggest != null) { 240 for (SuggestItem item : suggest) { 241 mixed.addResult(item); 242 } 243 } 244 return mixed; 245 } 246 247 class SuggestFilter extends Filter { 248 249 @Override 250 public CharSequence convertResultToString(Object item) { 251 if (item == null) { 252 return ""; 253 } 254 SuggestItem sitem = (SuggestItem) item; 255 if (sitem.title != null) { 256 return sitem.title; 257 } else { 258 return sitem.url; 259 } 260 } 261 262 void startSuggestionsAsync(final CharSequence constraint) { 263 if (!mIncognitoMode) { 264 new SlowFilterTask().execute(constraint); 265 } 266 } 267 268 private boolean shouldProcessEmptyQuery() { 269 final SearchEngine searchEngine = mSettings.getSearchEngine(); 270 return searchEngine.wantsEmptyQuery(); 271 } 272 273 @Override 274 protected FilterResults performFiltering(CharSequence constraint) { 275 FilterResults res = new FilterResults(); 276 if (TextUtils.isEmpty(constraint) && !shouldProcessEmptyQuery()) { 277 res.count = 0; 278 res.values = null; 279 return res; 280 } 281 startSuggestionsAsync(constraint); 282 List<SuggestItem> filterResults = new ArrayList<SuggestItem>(); 283 if (constraint != null) { 284 for (CursorSource sc : mSources) { 285 sc.runQuery(constraint); 286 } 287 mixResults(filterResults); 288 } 289 synchronized (mResultsLock) { 290 mFilterResults = filterResults; 291 } 292 SuggestionResults mixed = buildSuggestionResults(); 293 res.count = mixed.getLineCount(); 294 res.values = mixed; 295 return res; 296 } 297 298 void mixResults(List<SuggestItem> results) { 299 int maxLines = getMaxLines(); 300 for (int i = 0; i < mSources.size(); i++) { 301 CursorSource s = mSources.get(i); 302 int n = Math.min(s.getCount(), maxLines); 303 maxLines -= n; 304 boolean more = false; 305 for (int j = 0; j < n; j++) { 306 results.add(s.getItem()); 307 more = s.moveToNext(); 308 } 309 } 310 } 311 312 @Override 313 protected void publishResults(CharSequence constraint, FilterResults fresults) { 314 if (fresults.values instanceof SuggestionResults) { 315 mMixedResults = (SuggestionResults) fresults.values; 316 notifyDataSetChanged(); 317 } 318 } 319 } 320 321 private int getMaxLines() { 322 int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait; 323 maxLines = (int) Math.ceil(maxLines / 2.0); 324 return maxLines; 325 } 326 327 /** 328 * sorted list of results of a suggestion query 329 * 330 */ 331 class SuggestionResults { 332 333 ArrayList<SuggestItem> items; 334 // count per type 335 int[] counts; 336 337 SuggestionResults() { 338 items = new ArrayList<SuggestItem>(24); 339 // n of types: 340 counts = new int[5]; 341 } 342 343 int getTypeCount(int type) { 344 return counts[type]; 345 } 346 347 void addResult(SuggestItem item) { 348 int ix = 0; 349 while ((ix < items.size()) && (item.type >= items.get(ix).type)) 350 ix++; 351 items.add(ix, item); 352 counts[item.type]++; 353 } 354 355 int getLineCount() { 356 return Math.min((mLandscapeMode ? mLinesLandscape : mLinesPortrait), items.size()); 357 } 358 359 @Override 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 public class SuggestItem { 379 public String title; 380 public String url; 381 public int type; 382 public String extra; 383 384 public SuggestItem(String text, String u, int t) { 385 title = text; 386 url = u; 387 type = t; 388 } 389 390 } 391 392 abstract class CursorSource { 393 394 Cursor mCursor; 395 396 boolean moveToNext() { 397 return mCursor.moveToNext(); 398 } 399 400 public abstract void runQuery(CharSequence constraint); 401 402 public abstract SuggestItem getItem(); 403 404 public int getCount() { 405 return (mCursor != null) ? mCursor.getCount() : 0; 406 } 407 408 public void close() { 409 if (mCursor != null) { 410 mCursor.close(); 411 } 412 } 413 } 414 415 /** 416 * combined bookmark & history source 417 */ 418 class CombinedCursor extends CursorSource { 419 420 @Override 421 public SuggestItem getItem() { 422 if ((mCursor != null) && (!mCursor.isAfterLast())) { 423 String title = mCursor.getString(1); 424 String url = mCursor.getString(2); 425 boolean isBookmark = (mCursor.getInt(3) == 1); 426 return new SuggestItem(getTitle(title, url), getUrl(title, url), 427 isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY); 428 } 429 return null; 430 } 431 432 @Override 433 public void runQuery(CharSequence constraint) { 434 // constraint != null 435 if (mCursor != null) { 436 mCursor.close(); 437 } 438 String like = constraint + "%"; 439 String[] args = null; 440 String selection = null; 441 if (like.startsWith("http") || like.startsWith("file")) { 442 args = new String[1]; 443 args[0] = like; 444 selection = "url LIKE ?"; 445 } else { 446 args = new String[5]; 447 args[0] = "http://" + like; 448 args[1] = "http://www." + like; 449 args[2] = "https://" + like; 450 args[3] = "https://www." + like; 451 // To match against titles. 452 args[4] = like; 453 selection = COMBINED_SELECTION; 454 } 455 Uri.Builder ub = OmniboxSuggestions.CONTENT_URI.buildUpon(); 456 ub.appendQueryParameter(BrowserContract.PARAM_LIMIT, 457 Integer.toString(Math.max(mLinesLandscape, mLinesPortrait))); 458 mCursor = 459 mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION, 460 selection, (constraint != null) ? args : null, null); 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) { 474 title = UrlUtils.stripUrl(url); 475 } 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) { 487 if (TextUtils.isEmpty(title) 488 || TextUtils.getTrimmedLength(title) == 0 489 || title.equals(url)) { 490 return null; 491 } else { 492 return UrlUtils.stripUrl(url); 493 } 494 } 495 } 496 497 class SuggestCursor extends CursorSource { 498 499 @Override 500 public SuggestItem getItem() { 501 if (mCursor != null) { 502 String title = mCursor.getString( 503 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)); 504 String text2 = mCursor.getString( 505 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2)); 506 String url = mCursor.getString( 507 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL)); 508 String uri = mCursor.getString( 509 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA)); 510 int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL; 511 SuggestItem item = new SuggestItem(title, url, type); 512 item.extra = mCursor.getString( 513 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA)); 514 return item; 515 } 516 return null; 517 } 518 519 @Override 520 public void runQuery(CharSequence constraint) { 521 if (mCursor != null) { 522 mCursor.close(); 523 } 524 SearchEngine searchEngine = mSettings.getSearchEngine(); 525 if (!TextUtils.isEmpty(constraint)) { 526 if (searchEngine != null && searchEngine.supportsSuggestions()) { 527 mCursor = searchEngine.getSuggestions(mContext, constraint.toString()); 528 if (mCursor != null) { 529 mCursor.moveToFirst(); 530 } 531 } 532 } else { 533 if (searchEngine.wantsEmptyQuery()) { 534 mCursor = searchEngine.getSuggestions(mContext, ""); 535 } 536 mCursor = null; 537 } 538 } 539 540 } 541 542 public void clearCache() { 543 mFilterResults = null; 544 mSuggestResults = null; 545 notifyDataSetInvalidated(); 546 } 547 548 public void setIncognitoMode(boolean incognito) { 549 mIncognitoMode = incognito; 550 clearCache(); 551 } 552 553 static String getSuggestionTitle(SuggestItem item) { 554 // There must be a better way to strip HTML from things. 555 // This method is used in multiple places. It is also more 556 // expensive than a standard html escaper. 557 return (item.title != null) ? Html.fromHtml(item.title).toString() : null; 558 } 559 560 static String getSuggestionUrl(SuggestItem item) { 561 final String title = SuggestionsAdapter.getSuggestionTitle(item); 562 563 if (TextUtils.isEmpty(item.url)) { 564 return title; 565 } 566 567 return item.url; 568 } 569 } 570