/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.quicksearchbox; import com.android.quicksearchbox.util.CachedLater; import com.android.quicksearchbox.util.NamedTask; import com.android.quicksearchbox.util.NamedTaskExecutor; import com.android.quicksearchbox.util.Now; import com.android.quicksearchbox.util.NowOrLater; import com.android.quicksearchbox.util.Util; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.List; /** * Loads icons from other packages. * * Code partly stolen from {@link ContentResolver} and android.app.SuggestionsAdapter. */ public class PackageIconLoader implements IconLoader { private static final boolean DBG = false; private static final String TAG = "QSB.PackageIconLoader"; private final Context mContext; private final String mPackageName; private Context mPackageContext; private final Handler mUiThread; private final NamedTaskExecutor mIconLoaderExecutor; /** * Creates a new icon loader. * * @param context The QSB application context. * @param packageName The name of the package from which the icons will be loaded. * Resource IDs without an explicit package will be resolved against the package * of this context. */ public PackageIconLoader(Context context, String packageName, Handler uiThread, NamedTaskExecutor iconLoaderExecutor) { mContext = context; mPackageName = packageName; mUiThread = uiThread; mIconLoaderExecutor = iconLoaderExecutor; } private boolean ensurePackageContext() { if (mPackageContext == null) { try { mPackageContext = mContext.createPackageContext(mPackageName, Context.CONTEXT_RESTRICTED); } catch (PackageManager.NameNotFoundException ex) { // This should only happen if the app has just be uninstalled Log.e(TAG, "Application not found " + mPackageName); return false; } } return true; } public NowOrLater<Drawable> getIcon(final String drawableId) { if (DBG) Log.d(TAG, "getIcon(" + drawableId + ")"); if (TextUtils.isEmpty(drawableId) || "0".equals(drawableId)) { return new Now<Drawable>(null); } if (!ensurePackageContext()) { return new Now<Drawable>(null); } NowOrLater<Drawable> drawable; try { // First, see if it's just an integer int resourceId = Integer.parseInt(drawableId); // If so, find it by resource ID Drawable icon = mPackageContext.getResources().getDrawable(resourceId); drawable = new Now<Drawable>(icon); } catch (NumberFormatException nfe) { // It's not an integer, use it as a URI Uri uri = Uri.parse(drawableId); if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) { // load all resources synchronously, to reduce UI flickering drawable = new Now<Drawable>(getDrawable(uri)); } else { drawable = new IconLaterTask(uri); } } catch (Resources.NotFoundException nfe) { // It was an integer, but it couldn't be found, bail out Log.w(TAG, "Icon resource not found: " + drawableId); drawable = new Now<Drawable>(null); } return drawable; } public Uri getIconUri(String drawableId) { if (TextUtils.isEmpty(drawableId) || "0".equals(drawableId)) { return null; } if (!ensurePackageContext()) return null; try { int resourceId = Integer.parseInt(drawableId); return Util.getResourceUri(mPackageContext, resourceId); } catch (NumberFormatException nfe) { return Uri.parse(drawableId); } } /** * Gets a drawable by URI. * * @return A drawable, or {@code null} if the drawable could not be loaded. */ private Drawable getDrawable(Uri uri) { try { String scheme = uri.getScheme(); if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { // Load drawables through Resources, to get the source density information OpenResourceIdResult r = getResourceId(uri); try { return r.r.getDrawable(r.id); } catch (Resources.NotFoundException ex) { throw new FileNotFoundException("Resource does not exist: " + uri); } } else { // Let the ContentResolver handle content and file URIs. InputStream stream = mPackageContext.getContentResolver().openInputStream(uri); if (stream == null) { throw new FileNotFoundException("Failed to open " + uri); } try { return Drawable.createFromStream(stream, null); } finally { try { stream.close(); } catch (IOException ex) { Log.e(TAG, "Error closing icon stream for " + uri, ex); } } } } catch (FileNotFoundException fnfe) { Log.w(TAG, "Icon not found: " + uri + ", " + fnfe.getMessage()); return null; } } /** * A resource identified by the {@link Resources} that contains it, and a resource id. */ private class OpenResourceIdResult { public Resources r; public int id; } /** * Resolves an android.resource URI to a {@link Resources} and a resource id. */ private OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException { String authority = uri.getAuthority(); Resources r; if (TextUtils.isEmpty(authority)) { throw new FileNotFoundException("No authority: " + uri); } else { try { r = mPackageContext.getPackageManager().getResourcesForApplication(authority); } catch (NameNotFoundException ex) { throw new FileNotFoundException("Failed to get resources: " + ex); } } List<String> path = uri.getPathSegments(); if (path == null) { throw new FileNotFoundException("No path: " + uri); } int len = path.size(); int id; if (len == 1) { try { id = Integer.parseInt(path.get(0)); } catch (NumberFormatException e) { throw new FileNotFoundException("Single path segment is not a resource ID: " + uri); } } else if (len == 2) { id = r.getIdentifier(path.get(1), path.get(0), authority); } else { throw new FileNotFoundException("More than two path segments: " + uri); } if (id == 0) { throw new FileNotFoundException("No resource found for: " + uri); } OpenResourceIdResult res = new OpenResourceIdResult(); res.r = r; res.id = id; return res; } private class IconLaterTask extends CachedLater<Drawable> implements NamedTask { private final Uri mUri; public IconLaterTask(Uri iconUri) { mUri = iconUri; } @Override protected void create() { mIconLoaderExecutor.execute(this); } @Override public void run() { final Drawable icon = getIcon(); mUiThread.post(new Runnable(){ public void run() { store(icon); }}); } @Override public String getName() { return mPackageName; } private Drawable getIcon() { try { return getDrawable(mUri); } catch (Throwable t) { // we're making a call into another package, which could throw any exception. // Make sure it doesn't crash QSB Log.e(TAG, "Failed to load icon " + mUri, t); return null; } } } }