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