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