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