SuggestionsAdapter.java revision cfa3af5c59abb38c895416a80ef16da0ec1b5287
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 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.os.AsyncTask; 26import 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; 36import android.widget.TextView; 37 38import java.util.ArrayList; 39import java.util.List; 40 41/** 42 * adapter to wrap multiple cursors for url/search completions 43 */ 44public class SuggestionsAdapter extends BaseAdapter implements Filterable, OnClickListener { 45 46 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; 51 52 private static final String[] COMBINED_PROJECTION = 53 {BrowserContract.Combined._ID, BrowserContract.Combined.TITLE, 54 BrowserContract.Combined.URL, BrowserContract.Combined.IS_BOOKMARK}; 55 56 private static final String[] SEARCHES_PROJECTION = {BrowserContract.Searches.SEARCH}; 57 58 private static final String COMBINED_SELECTION = 59 "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)"; 60 61 Context mContext; 62 Filter mFilter; 63 SuggestionResults mMixedResults; 64 List<SuggestItem> mSuggestResults, mFilterResults; 65 List<CursorSource> mSources; 66 boolean mLandscapeMode; 67 CompletionListener mListener; 68 int mLinesPortrait; 69 int mLinesLandscape; 70 Object mResultsLock = new Object(); 71 List<String> mVoiceResults; 72 73 interface CompletionListener { 74 75 public void onSearch(String txt); 76 77 public void onSelect(String txt, String extraData); 78 79 public void onFilterComplete(int count); 80 81 } 82 83 public SuggestionsAdapter(Context ctx, CompletionListener listener) { 84 mContext = ctx; 85 mListener = listener; 86 mLinesPortrait = mContext.getResources(). 87 getInteger(R.integer.max_suggest_lines_portrait); 88 mLinesLandscape = mContext.getResources(). 89 getInteger(R.integer.max_suggest_lines_landscape); 90 mFilter = new SuggestFilter(); 91 addSource(new SearchesCursor()); 92 addSource(new CombinedCursor()); 93 } 94 95 void setVoiceResults(List<String> voiceResults) { 96 mVoiceResults = voiceResults; 97 notifyDataSetInvalidated(); 98 99 } 100 101 public void setLandscapeMode(boolean mode) { 102 mLandscapeMode = mode; 103 notifyDataSetChanged(); 104 } 105 106 public int getLeftCount() { 107 return mMixedResults.getLeftCount(); 108 } 109 110 public int getRightCount() { 111 return mMixedResults.getRightCount(); 112 } 113 114 public void addSource(CursorSource c) { 115 if (mSources == null) { 116 mSources = new ArrayList<CursorSource>(5); 117 } 118 mSources.add(c); 119 } 120 121 @Override 122 public void onClick(View v) { 123 if (R.id.icon2 == v.getId()) { 124 // replace input field text with suggestion text 125 SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag(); 126 mListener.onSearch(item.title); 127 } else { 128 SuggestItem item = (SuggestItem) v.getTag(); 129 mListener.onSelect((TextUtils.isEmpty(item.url)? item.title : item.url), 130 item.extra); 131 } 132 } 133 134 @Override 135 public Filter getFilter() { 136 return mFilter; 137 } 138 139 @Override 140 public int getCount() { 141 if (mVoiceResults != null) { 142 return mVoiceResults.size(); 143 } 144 return (mMixedResults == null) ? 0 : mMixedResults.getLineCount(); 145 } 146 147 @Override 148 public SuggestItem getItem(int position) { 149 if (mVoiceResults != null) { 150 return new SuggestItem(mVoiceResults.get(position), null, 151 TYPE_SEARCH); 152 } 153 if (mMixedResults == null) { 154 return null; 155 } 156 if (mLandscapeMode) { 157 if (position >= mMixedResults.getLineCount()) { 158 // right column 159 position = position - mMixedResults.getLineCount(); 160 // index in column 161 if (position >= mMixedResults.getRightCount()) { 162 return null; 163 } 164 return mMixedResults.items.get(position + mMixedResults.getLeftCount()); 165 } else { 166 // left column 167 if (position >= mMixedResults.getLeftCount()) { 168 return null; 169 } 170 return mMixedResults.items.get(position); 171 } 172 } else { 173 return mMixedResults.items.get(position); 174 } 175 } 176 177 @Override 178 public long getItemId(int position) { 179 return 0; 180 } 181 182 @Override 183 public View getView(int position, View convertView, ViewGroup parent) { 184 final LayoutInflater inflater = LayoutInflater.from(mContext); 185 View view = convertView; 186 if (view == null) { 187 view = inflater.inflate(R.layout.suggestion_two_column, parent, false); 188 } 189 View s1 = view.findViewById(R.id.suggest1); 190 View s2 = view.findViewById(R.id.suggest2); 191 View div = view.findViewById(R.id.suggestion_divider); 192 if (mLandscapeMode && (mVoiceResults == null)) { 193 SuggestItem item = getItem(position); 194 div.setVisibility(View.VISIBLE); 195 if (item != null) { 196 s1.setVisibility(View.VISIBLE); 197 bindView(s1, item); 198 } else { 199 s1.setVisibility(View.INVISIBLE); 200 } 201 item = getItem(position + mMixedResults.getLineCount()); 202 if (item != null) { 203 s2.setVisibility(View.VISIBLE); 204 bindView(s2, item); 205 } else { 206 s2.setVisibility(View.INVISIBLE); 207 } 208 return view; 209 } else { 210 s1.setVisibility(View.VISIBLE); 211 div.setVisibility(View.GONE); 212 s2.setVisibility(View.GONE); 213 bindView(s1, getItem(position)); 214 return view; 215 } 216 } 217 218 private void bindView(View view, SuggestItem item) { 219 // store item for click handling 220 view.setTag(item); 221 TextView tv1 = (TextView) view.findViewById(android.R.id.text1); 222 TextView tv2 = (TextView) view.findViewById(android.R.id.text2); 223 ImageView ic1 = (ImageView) view.findViewById(R.id.icon1); 224 View spacer = view.findViewById(R.id.spacer); 225 View ic2 = view.findViewById(R.id.icon2); 226 View div = view.findViewById(R.id.divider); 227 tv1.setText(item.title); 228 tv2.setText(item.url); 229 int id = -1; 230 switch (item.type) { 231 case TYPE_SUGGEST: 232 case TYPE_SEARCH: 233 id = R.drawable.ic_search_category_suggest; 234 break; 235 case TYPE_BOOKMARK: 236 id = R.drawable.ic_search_category_bookmark; 237 break; 238 case TYPE_HISTORY: 239 id = R.drawable.ic_search_category_history; 240 break; 241 case TYPE_SUGGEST_URL: 242 id = R.drawable.ic_search_category_browser; 243 break; 244 default: 245 id = -1; 246 } 247 if (id != -1) { 248 ic1.setImageDrawable(mContext.getResources().getDrawable(id)); 249 } 250 ic2.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type)) 251 ? View.VISIBLE : View.GONE); 252 div.setVisibility(ic2.getVisibility()); 253 spacer.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type)) 254 ? View.GONE : View.INVISIBLE); 255 view.setOnClickListener(this); 256 ic2.setOnClickListener(this); 257 } 258 259 class SlowFilterTask extends AsyncTask<CharSequence, Void, List<SuggestItem>> { 260 261 @Override 262 protected List<SuggestItem> doInBackground(CharSequence... params) { 263 SuggestCursor cursor = new SuggestCursor(); 264 cursor.runQuery(params[0]); 265 List<SuggestItem> results = new ArrayList<SuggestItem>(); 266 int count = cursor.getCount(); 267 for (int i = 0; i < count; i++) { 268 results.add(cursor.getItem()); 269 cursor.moveToNext(); 270 } 271 cursor.close(); 272 return results; 273 } 274 275 @Override 276 protected void onPostExecute(List<SuggestItem> items) { 277 mSuggestResults = items; 278 mMixedResults = buildSuggestionResults(); 279 notifyDataSetChanged(); 280 mListener.onFilterComplete(mMixedResults.getLineCount()); 281 } 282 } 283 284 SuggestionResults buildSuggestionResults() { 285 SuggestionResults mixed = new SuggestionResults(); 286 List<SuggestItem> filter, suggest; 287 synchronized (mResultsLock) { 288 filter = mFilterResults; 289 suggest = mSuggestResults; 290 } 291 if (filter != null) { 292 for (SuggestItem item : filter) { 293 mixed.addResult(item); 294 } 295 } 296 if (suggest != null) { 297 for (SuggestItem item : suggest) { 298 mixed.addResult(item); 299 } 300 } 301 return mixed; 302 } 303 304 class SuggestFilter extends Filter { 305 306 @Override 307 public CharSequence convertResultToString(Object item) { 308 if (item == null) { 309 return ""; 310 } 311 SuggestItem sitem = (SuggestItem) item; 312 if (sitem.title != null) { 313 return sitem.title; 314 } else { 315 return sitem.url; 316 } 317 } 318 319 void startSuggestionsAsync(final CharSequence constraint) { 320 new SlowFilterTask().execute(constraint); 321 } 322 323 @Override 324 protected FilterResults performFiltering(CharSequence constraint) { 325 FilterResults res = new FilterResults(); 326 if (mVoiceResults == null) { 327 if (TextUtils.isEmpty(constraint)) { 328 res.count = 0; 329 res.values = null; 330 return res; 331 } 332 startSuggestionsAsync(constraint); 333 List<SuggestItem> filterResults = new ArrayList<SuggestItem>(); 334 if (constraint != null) { 335 for (CursorSource sc : mSources) { 336 sc.runQuery(constraint); 337 } 338 mixResults(filterResults); 339 } 340 synchronized (mResultsLock) { 341 mFilterResults = filterResults; 342 } 343 SuggestionResults mixed = buildSuggestionResults(); 344 res.count = mixed.getLineCount(); 345 res.values = mixed; 346 } else { 347 res.count = mVoiceResults.size(); 348 res.values = mVoiceResults; 349 } 350 return res; 351 } 352 353 void mixResults(List<SuggestItem> results) { 354 int maxLines = mLandscapeMode ? mLinesLandscape : (mLinesPortrait / 2); 355 for (int i = 0; i < mSources.size(); i++) { 356 CursorSource s = mSources.get(i); 357 int n = Math.min(s.getCount(), maxLines); 358 maxLines -= n; 359 boolean more = false; 360 for (int j = 0; j < n; j++) { 361 results.add(s.getItem()); 362 more = s.moveToNext(); 363 } 364 } 365 } 366 367 @Override 368 protected void publishResults(CharSequence constraint, FilterResults fresults) { 369 if (fresults.values instanceof SuggestionResults) { 370 mMixedResults = (SuggestionResults) fresults.values; 371 mListener.onFilterComplete(fresults.count); 372 } 373 notifyDataSetChanged(); 374 } 375 376 } 377 378 /** 379 * sorted list of results of a suggestion query 380 * 381 */ 382 class SuggestionResults { 383 384 ArrayList<SuggestItem> items; 385 // count per type 386 int[] counts; 387 388 SuggestionResults() { 389 items = new ArrayList<SuggestItem>(24); 390 // n of types: 391 counts = new int[5]; 392 } 393 394 int getTypeCount(int type) { 395 return counts[type]; 396 } 397 398 void addResult(SuggestItem item) { 399 int ix = 0; 400 while ((ix < items.size()) && (item.type >= items.get(ix).type)) 401 ix++; 402 items.add(ix, item); 403 counts[item.type]++; 404 } 405 406 int getLineCount() { 407 if (mLandscapeMode) { 408 return Math.min(mLinesLandscape, 409 Math.max(getLeftCount(), getRightCount())); 410 } else { 411 return Math.min(mLinesPortrait, getLeftCount() + getRightCount()); 412 } 413 } 414 415 int getLeftCount() { 416 return counts[TYPE_BOOKMARK] + counts[TYPE_HISTORY] + counts[TYPE_SUGGEST_URL]; 417 } 418 419 int getRightCount() { 420 return counts[TYPE_SEARCH] + counts[TYPE_SUGGEST]; 421 } 422 423 @Override 424 public String toString() { 425 if (items == null) return null; 426 if (items.size() == 0) return "[]"; 427 StringBuilder sb = new StringBuilder(); 428 for (int i = 0; i < items.size(); i++) { 429 SuggestItem item = items.get(i); 430 sb.append(item.type + ": " + item.title); 431 if (i < items.size() - 1) { 432 sb.append(", "); 433 } 434 } 435 return sb.toString(); 436 } 437 } 438 439 /** 440 * data object to hold suggestion values 441 */ 442 class SuggestItem { 443 String title; 444 String url; 445 int type; 446 String extra; 447 448 public SuggestItem(String text, String u, int t) { 449 title = text; 450 url = u; 451 type = t; 452 } 453 } 454 455 abstract class CursorSource { 456 457 Cursor mCursor; 458 459 boolean moveToNext() { 460 return mCursor.moveToNext(); 461 } 462 463 public abstract void runQuery(CharSequence constraint); 464 465 public abstract SuggestItem getItem(); 466 467 public int getCount() { 468 return (mCursor != null) ? mCursor.getCount() : 0; 469 } 470 471 public void close() { 472 if (mCursor != null) { 473 mCursor.close(); 474 } 475 } 476 } 477 478 /** 479 * combined bookmark & history source 480 */ 481 class CombinedCursor extends CursorSource { 482 483 @Override 484 public SuggestItem getItem() { 485 if ((mCursor != null) && (!mCursor.isAfterLast())) { 486 String title = mCursor.getString(1); 487 String url = mCursor.getString(2); 488 boolean isBookmark = (mCursor.getInt(3) == 1); 489 return new SuggestItem(getTitle(title, url), getUrl(title, url), 490 isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY); 491 } 492 return null; 493 } 494 495 @Override 496 public void runQuery(CharSequence constraint) { 497 // constraint != null 498 if (mCursor != null) { 499 mCursor.close(); 500 } 501 String like = constraint + "%"; 502 String[] args = null; 503 String selection = null; 504 if (like.startsWith("http") || like.startsWith("file")) { 505 args = new String[1]; 506 args[0] = like; 507 selection = "url LIKE ?"; 508 } else { 509 args = new String[5]; 510 args[0] = "http://" + like; 511 args[1] = "http://www." + like; 512 args[2] = "https://" + like; 513 args[3] = "https://www." + like; 514 // To match against titles. 515 args[4] = like; 516 selection = COMBINED_SELECTION; 517 } 518 Uri.Builder ub = BrowserContract.Combined.CONTENT_URI.buildUpon(); 519 ub.appendQueryParameter(BrowserContract.PARAM_LIMIT, 520 Integer.toString(mLinesPortrait)); 521 BookmarkUtils.addAccountInfo(mContext, ub); 522 mCursor = 523 mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION, 524 selection, 525 (constraint != null) ? args : null, 526 BrowserContract.Combined.IS_BOOKMARK + " DESC, " + 527 BrowserContract.Combined.VISITS + " DESC, " + 528 BrowserContract.Combined.DATE_LAST_VISITED + " DESC"); 529 if (mCursor != null) { 530 mCursor.moveToFirst(); 531 } 532 } 533 534 /** 535 * Provides the title (text line 1) for a browser suggestion, which should be the 536 * webpage title. If the webpage title is empty, returns the stripped url instead. 537 * 538 * @return the title string to use 539 */ 540 private String getTitle(String title, String url) { 541 if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) { 542 title = UrlUtils.stripUrl(url); 543 } 544 return title; 545 } 546 547 /** 548 * Provides the subtitle (text line 2) for a browser suggestion, which should be the 549 * webpage url. If the webpage title is empty, then the url should go in the title 550 * instead, and the subtitle should be empty, so this would return null. 551 * 552 * @return the subtitle string to use, or null if none 553 */ 554 private String getUrl(String title, String url) { 555 if (TextUtils.isEmpty(title) 556 || TextUtils.getTrimmedLength(title) == 0 557 || title.equals(url)) { 558 return null; 559 } else { 560 return UrlUtils.stripUrl(url); 561 } 562 } 563 } 564 565 class SearchesCursor extends CursorSource { 566 567 @Override 568 public SuggestItem getItem() { 569 if ((mCursor != null) && (!mCursor.isAfterLast())) { 570 return new SuggestItem(mCursor.getString(0), null, TYPE_SEARCH); 571 } 572 return null; 573 } 574 575 @Override 576 public void runQuery(CharSequence constraint) { 577 // constraint != null 578 if (mCursor != null) { 579 mCursor.close(); 580 } 581 String like = constraint + "%"; 582 String[] args = new String[] {like}; 583 String selection = BrowserContract.Searches.SEARCH + " LIKE ?"; 584 Uri.Builder ub = BrowserContract.Searches.CONTENT_URI.buildUpon(); 585 ub.appendQueryParameter(BrowserContract.PARAM_LIMIT, 586 Integer.toString(mLinesPortrait)); 587 mCursor = 588 mContext.getContentResolver().query(ub.build(), SEARCHES_PROJECTION, 589 selection, 590 args, BrowserContract.Searches.DATE + " DESC"); 591 if (mCursor != null) { 592 mCursor.moveToFirst(); 593 } 594 } 595 596 } 597 598 class SuggestCursor extends CursorSource { 599 600 @Override 601 public SuggestItem getItem() { 602 if (mCursor != null) { 603 String title = mCursor.getString( 604 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)); 605 String text2 = mCursor.getString( 606 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2)); 607 String url = mCursor.getString( 608 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL)); 609 String uri = mCursor.getString( 610 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA)); 611 int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL; 612 SuggestItem item = new SuggestItem(title, url, type); 613 item.extra = mCursor.getString( 614 mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA)); 615 return item; 616 } 617 return null; 618 } 619 620 @Override 621 public void runQuery(CharSequence constraint) { 622 if (mCursor != null) { 623 mCursor.close(); 624 } 625 if (!TextUtils.isEmpty(constraint)) { 626 SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine(); 627 if (searchEngine != null && searchEngine.supportsSuggestions()) { 628 mCursor = searchEngine.getSuggestions(mContext, constraint.toString()); 629 if (mCursor != null) { 630 mCursor.moveToFirst(); 631 } 632 } 633 } else { 634 mCursor = null; 635 } 636 } 637 638 } 639 640 public void clearCache() { 641 mFilterResults = null; 642 mSuggestResults = null; 643 } 644 645} 646