1package android.support.v7.widget; 2 3/* 4 * Copyright (C) 2013 The Android Open Source Project 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19import android.app.SearchManager; 20import android.app.SearchableInfo; 21import android.content.ComponentName; 22import android.content.ContentResolver; 23import android.content.Context; 24import android.content.pm.ActivityInfo; 25import android.content.pm.PackageManager; 26import android.content.pm.PackageManager.NameNotFoundException; 27import android.content.res.ColorStateList; 28import android.content.res.Resources; 29import android.database.Cursor; 30import android.graphics.drawable.Drawable; 31import android.net.Uri; 32import android.os.Bundle; 33import android.support.v4.widget.ResourceCursorAdapter; 34import android.support.v7.appcompat.R; 35import android.text.Spannable; 36import android.text.SpannableString; 37import android.text.TextUtils; 38import android.text.style.TextAppearanceSpan; 39import android.util.Log; 40import android.util.TypedValue; 41import android.view.View; 42import android.view.View.OnClickListener; 43import android.view.ViewGroup; 44import android.widget.ImageView; 45import android.widget.TextView; 46 47import java.io.FileNotFoundException; 48import java.io.IOException; 49import java.io.InputStream; 50import java.util.List; 51import java.util.WeakHashMap; 52 53/** 54 * Provides the contents for the suggestion drop-down list.in {@link SearchView}. 55 * @hide 56 */ 57class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListener { 58 59 private static final boolean DBG = false; 60 private static final String LOG_TAG = "SuggestionsAdapter"; 61 private static final int QUERY_LIMIT = 50; 62 63 static final int REFINE_NONE = 0; 64 static final int REFINE_BY_ENTRY = 1; 65 static final int REFINE_ALL = 2; 66 67 private SearchManager mSearchManager; 68 private SearchView mSearchView; 69 private SearchableInfo mSearchable; 70 private Context mProviderContext; 71 private WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache; 72 private boolean mClosed = false; 73 private int mQueryRefinement = REFINE_BY_ENTRY; 74 75 // URL color 76 private ColorStateList mUrlColor; 77 78 static final int INVALID_INDEX = -1; 79 80 // Cached column indexes, updated when the cursor changes. 81 private int mText1Col = INVALID_INDEX; 82 private int mText2Col = INVALID_INDEX; 83 private int mText2UrlCol = INVALID_INDEX; 84 private int mIconName1Col = INVALID_INDEX; 85 private int mIconName2Col = INVALID_INDEX; 86 private int mFlagsCol = INVALID_INDEX; 87 88 // private final Runnable mStartSpinnerRunnable; 89 // private final Runnable mStopSpinnerRunnable; 90 91 public SuggestionsAdapter(Context context, SearchView searchView, 92 SearchableInfo searchable, 93 WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) { 94 super(context, R.layout.abc_search_dropdown_item_icons_2line, 95 null, // no initial cursor 96 true); // auto-requery 97 mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); 98 mSearchView = searchView; 99 mSearchable = searchable; 100 // set up provider resources (gives us icons, etc.) 101 mProviderContext = context; 102 103 mOutsideDrawablesCache = outsideDrawablesCache; 104 } 105 106 /** 107 * Enables query refinement for all suggestions. This means that an additional icon 108 * will be shown for each entry. When clicked, the suggested text on that line will be 109 * copied to the query text field. 110 * <p> 111 * 112 * @param refineWhat which queries to refine. Possible values are {@link #REFINE_NONE}, 113 * {@link #REFINE_BY_ENTRY}, and {@link #REFINE_ALL}. 114 */ 115 public void setQueryRefinement(int refineWhat) { 116 mQueryRefinement = refineWhat; 117 } 118 119 /** 120 * Returns the current query refinement preference. 121 * @return value of query refinement preference 122 */ 123 public int getQueryRefinement() { 124 return mQueryRefinement; 125 } 126 127 /** 128 * Overridden to always return <code>false</code>, since we cannot be sure that 129 * suggestion sources return stable IDs. 130 */ 131 @Override 132 public boolean hasStableIds() { 133 return false; 134 } 135 136 /** 137 * Use the search suggestions provider to obtain a live cursor. This will be called 138 * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions). 139 * The results will be processed in the UI thread and changeCursor() will be called. 140 */ 141 @Override 142 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 143 if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")"); 144 String query = (constraint == null) ? "" : constraint.toString(); 145 /** 146 * for in app search we show the progress spinner until the cursor is returned with 147 * the results. 148 */ 149 Cursor cursor = null; 150 if (mSearchView.getVisibility() != View.VISIBLE 151 || mSearchView.getWindowVisibility() != View.VISIBLE) { 152 return null; 153 } 154 try { 155 cursor = getSearchManagerSuggestions(mSearchable, query, QUERY_LIMIT); 156 // trigger fill window so the spinner stays up until the results are copied over and 157 // closer to being ready 158 if (cursor != null) { 159 cursor.getCount(); 160 return cursor; 161 } 162 } catch (RuntimeException e) { 163 Log.w(LOG_TAG, "Search suggestions query threw an exception.", e); 164 } 165 // If cursor is null or an exception was thrown, stop the spinner and return null. 166 // changeCursor doesn't get called if cursor is null 167 return null; 168 } 169 170 public void close() { 171 if (DBG) Log.d(LOG_TAG, "close()"); 172 changeCursor(null); 173 mClosed = true; 174 } 175 176 @Override 177 public void notifyDataSetChanged() { 178 if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged"); 179 super.notifyDataSetChanged(); 180 181 updateSpinnerState(getCursor()); 182 } 183 184 @Override 185 public void notifyDataSetInvalidated() { 186 if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated"); 187 super.notifyDataSetInvalidated(); 188 189 updateSpinnerState(getCursor()); 190 } 191 192 private void updateSpinnerState(Cursor cursor) { 193 Bundle extras = cursor != null ? cursor.getExtras() : null; 194 if (DBG) { 195 Log.d(LOG_TAG, "updateSpinnerState - extra = " 196 + (extras != null 197 ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS) 198 : null)); 199 } 200 // Check if the Cursor indicates that the query is not complete and show the spinner 201 if (extras != null 202 && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) { 203 return; 204 } 205 // If cursor is null or is done, stop the spinner 206 } 207 208 /** 209 * Cache columns. 210 */ 211 @Override 212 public void changeCursor(Cursor c) { 213 if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")"); 214 215 if (mClosed) { 216 Log.w(LOG_TAG, "Tried to change cursor after adapter was closed."); 217 if (c != null) c.close(); 218 return; 219 } 220 221 try { 222 super.changeCursor(c); 223 224 if (c != null) { 225 mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); 226 mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); 227 mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL); 228 mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); 229 mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); 230 mFlagsCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FLAGS); 231 } 232 } catch (Exception e) { 233 Log.e(LOG_TAG, "error changing cursor and caching columns", e); 234 } 235 } 236 237 /** 238 * Tags the view with cached child view look-ups. 239 */ 240 @Override 241 public View newView(Context context, Cursor cursor, ViewGroup parent) { 242 View v = super.newView(context, cursor, parent); 243 v.setTag(new ChildViewCache(v)); 244 return v; 245 } 246 247 /** 248 * Cache of the child views of drop-drown list items, to avoid looking up the children 249 * each time the contents of a list item are changed. 250 */ 251 private final static class ChildViewCache { 252 public final TextView mText1; 253 public final TextView mText2; 254 public final ImageView mIcon1; 255 public final ImageView mIcon2; 256 public final ImageView mIconRefine; 257 258 public ChildViewCache(View v) { 259 mText1 = (TextView) v.findViewById(android.R.id.text1); 260 mText2 = (TextView) v.findViewById(android.R.id.text2); 261 mIcon1 = (ImageView) v.findViewById(android.R.id.icon1); 262 mIcon2 = (ImageView) v.findViewById(android.R.id.icon2); 263 mIconRefine = (ImageView) v.findViewById(R.id.edit_query); 264 } 265 } 266 267 @Override 268 public void bindView(View view, Context context, Cursor cursor) { 269 ChildViewCache views = (ChildViewCache) view.getTag(); 270 271 int flags = 0; 272 if (mFlagsCol != INVALID_INDEX) { 273 flags = cursor.getInt(mFlagsCol); 274 } 275 if (views.mText1 != null) { 276 String text1 = getStringOrNull(cursor, mText1Col); 277 setViewText(views.mText1, text1); 278 } 279 if (views.mText2 != null) { 280 // First check TEXT_2_URL 281 CharSequence text2 = getStringOrNull(cursor, mText2UrlCol); 282 if (text2 != null) { 283 text2 = formatUrl(text2); 284 } else { 285 text2 = getStringOrNull(cursor, mText2Col); 286 } 287 288 // If no second line of text is indicated, allow the first line of text 289 // to be up to two lines if it wants to be. 290 if (TextUtils.isEmpty(text2)) { 291 if (views.mText1 != null) { 292 views.mText1.setSingleLine(false); 293 views.mText1.setMaxLines(2); 294 } 295 } else { 296 if (views.mText1 != null) { 297 views.mText1.setSingleLine(true); 298 views.mText1.setMaxLines(1); 299 } 300 } 301 setViewText(views.mText2, text2); 302 } 303 304 if (views.mIcon1 != null) { 305 setViewDrawable(views.mIcon1, getIcon1(cursor), View.INVISIBLE); 306 } 307 if (views.mIcon2 != null) { 308 setViewDrawable(views.mIcon2, getIcon2(cursor), View.GONE); 309 } 310 if (mQueryRefinement == REFINE_ALL 311 || (mQueryRefinement == REFINE_BY_ENTRY 312 && (flags & SearchManager.FLAG_QUERY_REFINEMENT) != 0)) { 313 views.mIconRefine.setVisibility(View.VISIBLE); 314 views.mIconRefine.setTag(views.mText1.getText()); 315 views.mIconRefine.setOnClickListener(this); 316 } else { 317 views.mIconRefine.setVisibility(View.GONE); 318 } 319 } 320 321 public void onClick(View v) { 322 Object tag = v.getTag(); 323 if (tag instanceof CharSequence) { 324 mSearchView.onQueryRefine((CharSequence) tag); 325 } 326 } 327 328 private CharSequence formatUrl(CharSequence url) { 329 if (mUrlColor == null) { 330 // Lazily get the URL color from the current theme. 331 TypedValue colorValue = new TypedValue(); 332 mContext.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true); 333 mUrlColor = mContext.getResources().getColorStateList(colorValue.resourceId); 334 } 335 336 SpannableString text = new SpannableString(url); 337 text.setSpan(new TextAppearanceSpan(null, 0, 0, mUrlColor, null), 338 0, url.length(), 339 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 340 return text; 341 } 342 343 private void setViewText(TextView v, CharSequence text) { 344 // Set the text even if it's null, since we need to clear any previous text. 345 v.setText(text); 346 347 if (TextUtils.isEmpty(text)) { 348 v.setVisibility(View.GONE); 349 } else { 350 v.setVisibility(View.VISIBLE); 351 } 352 } 353 354 private Drawable getIcon1(Cursor cursor) { 355 if (mIconName1Col == INVALID_INDEX) { 356 return null; 357 } 358 String value = cursor.getString(mIconName1Col); 359 Drawable drawable = getDrawableFromResourceValue(value); 360 if (drawable != null) { 361 return drawable; 362 } 363 return getDefaultIcon1(cursor); 364 } 365 366 private Drawable getIcon2(Cursor cursor) { 367 if (mIconName2Col == INVALID_INDEX) { 368 return null; 369 } 370 String value = cursor.getString(mIconName2Col); 371 return getDrawableFromResourceValue(value); 372 } 373 374 /** 375 * Sets the drawable in an image view, makes sure the view is only visible if there 376 * is a drawable. 377 */ 378 private void setViewDrawable(ImageView v, Drawable drawable, int nullVisibility) { 379 // Set the icon even if the drawable is null, since we need to clear any 380 // previous icon. 381 v.setImageDrawable(drawable); 382 383 if (drawable == null) { 384 v.setVisibility(nullVisibility); 385 } else { 386 v.setVisibility(View.VISIBLE); 387 388 // This is a hack to get any animated drawables (like a 'working' spinner) 389 // to animate. You have to setVisible true on an AnimationDrawable to get 390 // it to start animating, but it must first have been false or else the 391 // call to setVisible will be ineffective. We need to clear up the story 392 // about animated drawables in the future, see http://b/1878430. 393 drawable.setVisible(false, false); 394 drawable.setVisible(true, false); 395 } 396 } 397 398 /** 399 * Gets the text to show in the query field when a suggestion is selected. 400 * 401 * @param cursor The Cursor to read the suggestion data from. The Cursor should already 402 * be moved to the suggestion that is to be read from. 403 * @return The text to show, or <code>null</code> if the query should not be 404 * changed when selecting this suggestion. 405 */ 406 @Override 407 public CharSequence convertToString(Cursor cursor) { 408 if (cursor == null) { 409 return null; 410 } 411 412 String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY); 413 if (query != null) { 414 return query; 415 } 416 417 if (mSearchable.shouldRewriteQueryFromData()) { 418 String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA); 419 if (data != null) { 420 return data; 421 } 422 } 423 424 if (mSearchable.shouldRewriteQueryFromText()) { 425 String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1); 426 if (text1 != null) { 427 return text1; 428 } 429 } 430 431 return null; 432 } 433 434 /** 435 * This method is overridden purely to provide a bit of protection against 436 * flaky content providers. 437 * 438 * @see android.widget.ListAdapter#getView(int, View, ViewGroup) 439 */ 440 @Override 441 public View getView(int position, View convertView, ViewGroup parent) { 442 try { 443 return super.getView(position, convertView, parent); 444 } catch (RuntimeException e) { 445 Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e); 446 // Put exception string in item title 447 View v = newView(mContext, mCursor, parent); 448 if (v != null) { 449 ChildViewCache views = (ChildViewCache) v.getTag(); 450 TextView tv = views.mText1; 451 tv.setText(e.toString()); 452 } 453 return v; 454 } 455 } 456 457 /** 458 * Gets a drawable given a value provided by a suggestion provider. 459 * 460 * This value could be just the string value of a resource id 461 * (e.g., "2130837524"), in which case we will try to retrieve a drawable from 462 * the provider's resources. If the value is not an integer, it is 463 * treated as a Uri and opened with 464 * {@link ContentResolver#openOutputStream(android.net.Uri, String)}. 465 * 466 * All resources and URIs are read using the suggestion provider's context. 467 * 468 * If the string is not formatted as expected, or no drawable can be found for 469 * the provided value, this method returns null. 470 * 471 * @param drawableId a string like "2130837524", 472 * "android.resource://com.android.alarmclock/2130837524", 473 * or "content://contacts/photos/253". 474 * @return a Drawable, or null if none found 475 */ 476 private Drawable getDrawableFromResourceValue(String drawableId) { 477 if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) { 478 return null; 479 } 480 try { 481 // First, see if it's just an integer 482 int resourceId = Integer.parseInt(drawableId); 483 // It's an int, look for it in the cache 484 String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE 485 + "://" + mProviderContext.getPackageName() + "/" + resourceId; 486 // Must use URI as cache key, since ints are app-specific 487 Drawable drawable = checkIconCache(drawableUri); 488 if (drawable != null) { 489 return drawable; 490 } 491 // Not cached, find it by resource ID 492 drawable = mProviderContext.getResources().getDrawable(resourceId); 493 // Stick it in the cache, using the URI as key 494 storeInIconCache(drawableUri, drawable); 495 return drawable; 496 } catch (NumberFormatException nfe) { 497 // It's not an integer, use it as a URI 498 Drawable drawable = checkIconCache(drawableId); 499 if (drawable != null) { 500 return drawable; 501 } 502 Uri uri = Uri.parse(drawableId); 503 drawable = getDrawable(uri); 504 storeInIconCache(drawableId, drawable); 505 return drawable; 506 } catch (Resources.NotFoundException nfe) { 507 // It was an integer, but it couldn't be found, bail out 508 Log.w(LOG_TAG, "Icon resource not found: " + drawableId); 509 return null; 510 } 511 } 512 513 /** 514 * Gets a drawable by URI, without using the cache. 515 * 516 * @return A drawable, or {@code null} if the drawable could not be loaded. 517 */ 518 private Drawable getDrawable(Uri uri) { 519 try { 520 String scheme = uri.getScheme(); 521 if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { 522 // Load drawables through Resources, to get the source density information 523 try { 524 return getDrawableFromResourceUri(uri); 525 } catch (Resources.NotFoundException ex) { 526 throw new FileNotFoundException("Resource does not exist: " + uri); 527 } 528 } else { 529 // Let the ContentResolver handle content and file URIs. 530 InputStream stream = mProviderContext.getContentResolver().openInputStream(uri); 531 if (stream == null) { 532 throw new FileNotFoundException("Failed to open " + uri); 533 } 534 try { 535 return Drawable.createFromStream(stream, null); 536 } finally { 537 try { 538 stream.close(); 539 } catch (IOException ex) { 540 Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex); 541 } 542 } 543 } 544 } catch (FileNotFoundException fnfe) { 545 Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage()); 546 return null; 547 } 548 } 549 550 551 552 private Drawable checkIconCache(String resourceUri) { 553 Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri); 554 if (cached == null) { 555 return null; 556 } 557 if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri); 558 return cached.newDrawable(); 559 } 560 561 private void storeInIconCache(String resourceUri, Drawable drawable) { 562 if (drawable != null) { 563 mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState()); 564 } 565 } 566 567 /** 568 * Gets the left-hand side icon that will be used for the current suggestion 569 * if the suggestion contains an icon column but no icon or a broken icon. 570 * 571 * @param cursor A cursor positioned at the current suggestion. 572 * @return A non-null drawable. 573 */ 574 private Drawable getDefaultIcon1(Cursor cursor) { 575 // Check the component that gave us the suggestion 576 Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity()); 577 if (drawable != null) { 578 return drawable; 579 } 580 581 // Fall back to a default icon 582 return mContext.getPackageManager().getDefaultActivityIcon(); 583 } 584 585 /** 586 * Gets the activity or application icon for an activity. 587 * Uses the local icon cache for fast repeated lookups. 588 * 589 * @param component Name of an activity. 590 * @return A drawable, or {@code null} if neither the activity nor the application 591 * has an icon set. 592 */ 593 private Drawable getActivityIconWithCache(ComponentName component) { 594 // First check the icon cache 595 String componentIconKey = component.flattenToShortString(); 596 // Using containsKey() since we also store null values. 597 if (mOutsideDrawablesCache.containsKey(componentIconKey)) { 598 Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey); 599 return cached == null ? null : cached.newDrawable(mProviderContext.getResources()); 600 } 601 // Then try the activity or application icon 602 Drawable drawable = getActivityIcon(component); 603 // Stick it in the cache so we don't do this lookup again. 604 Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState(); 605 mOutsideDrawablesCache.put(componentIconKey, toCache); 606 return drawable; 607 } 608 609 /** 610 * Gets the activity or application icon for an activity. 611 * 612 * @param component Name of an activity. 613 * @return A drawable, or {@code null} if neither the acitivy or the application 614 * have an icon set. 615 */ 616 private Drawable getActivityIcon(ComponentName component) { 617 PackageManager pm = mContext.getPackageManager(); 618 final ActivityInfo activityInfo; 619 try { 620 activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA); 621 } catch (NameNotFoundException ex) { 622 Log.w(LOG_TAG, ex.toString()); 623 return null; 624 } 625 int iconId = activityInfo.getIconResource(); 626 if (iconId == 0) return null; 627 String pkg = component.getPackageName(); 628 Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo); 629 if (drawable == null) { 630 Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for " 631 + component.flattenToShortString()); 632 return null; 633 } 634 return drawable; 635 } 636 637 /** 638 * Gets the value of a string column by name. 639 * 640 * @param cursor Cursor to read the value from. 641 * @param columnName The name of the column to read. 642 * @return The value of the given column, or <code>null</null> 643 * if the cursor does not contain the given column. 644 */ 645 public static String getColumnString(Cursor cursor, String columnName) { 646 int col = cursor.getColumnIndex(columnName); 647 return getStringOrNull(cursor, col); 648 } 649 650 private static String getStringOrNull(Cursor cursor, int col) { 651 if (col == INVALID_INDEX) { 652 return null; 653 } 654 try { 655 return cursor.getString(col); 656 } catch (Exception e) { 657 Log.e(LOG_TAG, 658 "unexpected error retrieving valid column from cursor, " 659 + "did the remote process die?", e); 660 return null; 661 } 662 } 663 664 /** 665 * Import of hidden method: ContentResolver.getResourceId(Uri). 666 * Modified to return a drawable, rather than a hidden type. 667 */ 668 Drawable getDrawableFromResourceUri(Uri uri) throws FileNotFoundException { 669 String authority = uri.getAuthority(); 670 Resources r; 671 if (TextUtils.isEmpty(authority)) { 672 throw new FileNotFoundException("No authority: " + uri); 673 } else { 674 try { 675 r = mContext.getPackageManager().getResourcesForApplication(authority); 676 } catch (NameNotFoundException ex) { 677 throw new FileNotFoundException("No package found for authority: " + uri); 678 } 679 } 680 List<String> path = uri.getPathSegments(); 681 if (path == null) { 682 throw new FileNotFoundException("No path: " + uri); 683 } 684 int len = path.size(); 685 int id; 686 if (len == 1) { 687 try { 688 id = Integer.parseInt(path.get(0)); 689 } catch (NumberFormatException e) { 690 throw new FileNotFoundException("Single path segment is not a resource ID: " + uri); 691 } 692 } else if (len == 2) { 693 id = r.getIdentifier(path.get(1), path.get(0), authority); 694 } else { 695 throw new FileNotFoundException("More than two path segments: " + uri); 696 } 697 if (id == 0) { 698 throw new FileNotFoundException("No resource found for: " + uri); 699 } 700 return r.getDrawable(id); 701 } 702 703 /** 704 * Import of hidden method: SearchManager.getSuggestions(SearchableInfo, String, int). 705 */ 706 Cursor getSearchManagerSuggestions(SearchableInfo searchable, String query, int limit) { 707 if (searchable == null) { 708 return null; 709 } 710 711 String authority = searchable.getSuggestAuthority(); 712 if (authority == null) { 713 return null; 714 } 715 716 Uri.Builder uriBuilder = new Uri.Builder() 717 .scheme(ContentResolver.SCHEME_CONTENT) 718 .authority(authority) 719 .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() 720 .fragment(""); // TODO: Remove, workaround for a bug in Uri.writeToParcel() 721 722 // if content path provided, insert it now 723 final String contentPath = searchable.getSuggestPath(); 724 if (contentPath != null) { 725 uriBuilder.appendEncodedPath(contentPath); 726 } 727 728 // append standard suggestion query path 729 uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY); 730 731 // get the query selection, may be null 732 String selection = searchable.getSuggestSelection(); 733 // inject query, either as selection args or inline 734 String[] selArgs = null; 735 if (selection != null) { // use selection if provided 736 selArgs = new String[] { query }; 737 } else { // no selection, use REST pattern 738 uriBuilder.appendPath(query); 739 } 740 741 if (limit > 0) { 742 uriBuilder.appendQueryParameter("limit", String.valueOf(limit)); 743 } 744 745 Uri uri = uriBuilder.build(); 746 747 // finally, make the query 748 return mContext.getContentResolver().query(uri, null, selection, selArgs, null); 749 } 750}