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