1/* 2 * Copyright (C) 2016 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 */ 16package com.android.car.apps.common; 17 18import android.accounts.Account; 19import android.accounts.AccountManager; 20import android.content.Context; 21import android.content.Intent.ShortcutIconResource; 22import android.content.pm.PackageManager.NameNotFoundException; 23import android.content.res.Resources; 24import android.content.res.Resources.NotFoundException; 25import android.graphics.Bitmap; 26import android.graphics.BitmapFactory; 27import android.graphics.drawable.Drawable; 28import android.net.Uri; 29import android.os.AsyncTask; 30import android.util.Log; 31import android.util.TypedValue; 32import android.widget.ImageView; 33 34import java.io.FileNotFoundException; 35import java.io.IOException; 36import java.io.InputStream; 37import java.lang.ref.WeakReference; 38import java.net.SocketTimeoutException; 39import java.net.URL; 40import java.net.URLConnection; 41 42/** 43 * AsyncTask which loads a bitmap. 44 * <p> 45 * The source of this can be another package (via a resource), a URI (content provider), or 46 * a file path. 47 * @see BitmapWorkerOptions 48 * @hide 49 */ 50class DrawableLoader extends AsyncTask<BitmapWorkerOptions, Void, Drawable> { 51 52 private static final String TAG = "DrawableLoader"; 53 private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; 54 55 private static final boolean DEBUG = false; 56 57 private static final int SOCKET_TIMEOUT = 10000; 58 private static final int READ_TIMEOUT = 10000; 59 60 private final WeakReference<ImageView> mImageView; 61 private int mOriginalWidth; 62 private int mOriginalHeight; 63 private final RecycleBitmapPool mRecycledBitmaps; 64 65 private final RefcountObject.RefcountListener mRefcountListener = 66 new RefcountObject.RefcountListener() { 67 @Override 68 @SuppressWarnings("rawtypes") 69 public void onRefcountZero(RefcountObject object) { 70 mRecycledBitmaps.addRecycledBitmap((Bitmap) object.getObject()); 71 } 72 }; 73 74 75 DrawableLoader(ImageView imageView, RecycleBitmapPool recycledBitmapPool) { 76 mImageView = new WeakReference<ImageView>(imageView); 77 mRecycledBitmaps = recycledBitmapPool; 78 } 79 80 public int getOriginalWidth() { 81 return mOriginalWidth; 82 } 83 84 public int getOriginalHeight() { 85 return mOriginalHeight; 86 } 87 88 @Override 89 protected Drawable doInBackground(BitmapWorkerOptions... params) { 90 91 return retrieveDrawable(params[0]); 92 } 93 94 protected Drawable retrieveDrawable(BitmapWorkerOptions workerOptions) { 95 try { 96 if (workerOptions.getIconResource() != null) { 97 return getBitmapFromResource(workerOptions.getIconResource(), workerOptions); 98 } else if (workerOptions.getResourceUri() != null) { 99 if (UriUtils.isAndroidResourceUri(workerOptions.getResourceUri()) 100 || UriUtils.isShortcutIconResourceUri(workerOptions.getResourceUri())) { 101 // Make an icon resource from this. 102 return getBitmapFromResource( 103 UriUtils.getIconResource(workerOptions.getResourceUri()), 104 workerOptions); 105 } else if (UriUtils.isWebUri(workerOptions.getResourceUri())) { 106 return getBitmapFromHttp(workerOptions); 107 } else if (UriUtils.isContentUri(workerOptions.getResourceUri())) { 108 return getBitmapFromContent(workerOptions); 109 } else if (UriUtils.isAccountImageUri(workerOptions.getResourceUri())) { 110 return getAccountImage(workerOptions); 111 } else { 112 Log.e(TAG, "Error loading bitmap - unknown resource URI! " 113 + workerOptions.getResourceUri()); 114 } 115 } else { 116 Log.e(TAG, "Error loading bitmap - no source!"); 117 } 118 } catch (IOException e) { 119 Log.e(TAG, "Error loading url " + workerOptions.getResourceUri(), e); 120 return null; 121 } catch (RuntimeException e) { 122 Log.e(TAG, "Critical Error loading url " + workerOptions.getResourceUri(), e); 123 return null; 124 } 125 126 return null; 127 } 128 129 @Override 130 protected void onPostExecute(Drawable bitmap) { 131 if (mImageView != null) { 132 final ImageView imageView = mImageView.get(); 133 if (imageView != null) { 134 imageView.setImageDrawable(bitmap); 135 } 136 } 137 } 138 139 @Override 140 protected void onCancelled(Drawable result) { 141 if (result instanceof RefcountBitmapDrawable) { 142 // Remove the extra refcount created by us, DrawableDownloader LruCache 143 // still holds one to the bitmap 144 RefcountBitmapDrawable d = (RefcountBitmapDrawable) result; 145 d.getRefcountObject().releaseRef(); 146 } 147 } 148 149 private Drawable getBitmapFromResource(ShortcutIconResource iconResource, 150 BitmapWorkerOptions outputOptions) throws IOException { 151 if (DEBUG) { 152 Log.d(TAG, "Loading " + iconResource.toString()); 153 } 154 try { 155 Object drawable = loadDrawable(outputOptions.getContext(), iconResource); 156 if (drawable instanceof InputStream) { 157 // Most of these are bitmaps, so resize properly. 158 return decodeBitmap((InputStream)drawable, outputOptions); 159 } else if (drawable instanceof Drawable){ 160 Drawable d = (Drawable) drawable; 161 mOriginalWidth = d.getIntrinsicWidth(); 162 mOriginalHeight = d.getIntrinsicHeight(); 163 return d; 164 } else { 165 Log.w(TAG, "getBitmapFromResource failed, unrecognized resource: " + drawable); 166 return null; 167 } 168 } catch (NameNotFoundException e) { 169 Log.w(TAG, "Could not load package: " + iconResource.packageName + "! NameNotFound"); 170 return null; 171 } catch (NotFoundException e) { 172 Log.w(TAG, "Could not load resource: " + iconResource.resourceName + "! NotFound"); 173 return null; 174 } 175 } 176 177 private Drawable decodeBitmap(InputStream in, BitmapWorkerOptions options) 178 throws IOException { 179 CachedInputStream bufferedStream = null; 180 BitmapFactory.Options bitmapOptions = null; 181 try { 182 bufferedStream = new CachedInputStream(in); 183 // Let the bufferedStream be able to mark unlimited bytes up to full stream length. 184 // The value that BitmapFactory uses (1024) is too small for detecting bounds 185 bufferedStream.setOverrideMarkLimit(Integer.MAX_VALUE); 186 bitmapOptions = new BitmapFactory.Options(); 187 bitmapOptions.inJustDecodeBounds = true; 188 if (options.getBitmapConfig() != null) { 189 bitmapOptions.inPreferredConfig = options.getBitmapConfig(); 190 } 191 bitmapOptions.inTempStorage = ByteArrayPool.get16KBPool().allocateChunk(); 192 bufferedStream.mark(Integer.MAX_VALUE); 193 BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions); 194 195 mOriginalWidth = bitmapOptions.outWidth; 196 mOriginalHeight = bitmapOptions.outHeight; 197 int heightScale = 1; 198 int height = options.getHeight(); 199 if (height > 0) { 200 heightScale = bitmapOptions.outHeight / height; 201 } 202 203 int widthScale = 1; 204 int width = options.getWidth(); 205 if (width > 0) { 206 widthScale = bitmapOptions.outWidth / width; 207 } 208 209 int scale = heightScale > widthScale ? heightScale : widthScale; 210 if (scale <= 1) { 211 scale = 1; 212 } else { 213 int shift = 0; 214 do { 215 scale >>= 1; 216 shift++; 217 } while (scale != 0); 218 scale = 1 << (shift - 1); 219 } 220 221 if (DEBUG) { 222 Log.d("BitmapWorkerTask", "Source bitmap: (" + bitmapOptions.outWidth + "x" 223 + bitmapOptions.outHeight + "). Max size: (" + options.getWidth() + "x" 224 + options.getHeight() + "). Chosen scale: " + scale + " -> " + scale); 225 } 226 227 // Reset buffer to original position and disable the overrideMarkLimit 228 bufferedStream.reset(); 229 bufferedStream.setOverrideMarkLimit(0); 230 Bitmap bitmap = null; 231 try { 232 bitmapOptions.inJustDecodeBounds = false; 233 bitmapOptions.inSampleSize = scale; 234 bitmapOptions.inMutable = true; 235 bitmapOptions.inBitmap = mRecycledBitmaps.getRecycledBitmap( 236 mOriginalWidth / scale, mOriginalHeight / scale); 237 bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions); 238 } catch (RuntimeException ex) { 239 Log.e(TAG, "RuntimeException" + ex + ", trying decodeStream again"); 240 bufferedStream.reset(); 241 bufferedStream.setOverrideMarkLimit(0); 242 bitmapOptions.inBitmap = null; 243 bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions); 244 } 245 if (bitmap == null) { 246 Log.d(TAG, "bitmap was null"); 247 return null; 248 } 249 RefcountObject<Bitmap> object = new RefcountObject<Bitmap>(bitmap); 250 object.addRef(); 251 object.setRefcountListener(mRefcountListener); 252 RefcountBitmapDrawable d = new RefcountBitmapDrawable( 253 options.getContext().getResources(), object); 254 return d; 255 } finally { 256 Log.w(TAG, "couldn't load bitmap, releasing resources"); 257 if (bitmapOptions != null) { 258 ByteArrayPool.get16KBPool().releaseChunk(bitmapOptions.inTempStorage); 259 } 260 if (bufferedStream != null) { 261 bufferedStream.close(); 262 } 263 } 264 } 265 266 private Drawable getBitmapFromHttp(BitmapWorkerOptions options) throws IOException { 267 URL url = new URL(options.getResourceUri().toString()); 268 if (DEBUG) { 269 Log.d(TAG, "Loading " + url); 270 } 271 try { 272 // TODO use volley for better disk cache 273 URLConnection connection = url.openConnection(); 274 connection.setConnectTimeout(SOCKET_TIMEOUT); 275 connection.setReadTimeout(READ_TIMEOUT); 276 InputStream in = connection.getInputStream(); 277 return decodeBitmap(in, options); 278 } catch (SocketTimeoutException e) { 279 Log.e(TAG, "loading " + url + " timed out"); 280 } 281 return null; 282 } 283 284 private Drawable getBitmapFromContent(BitmapWorkerOptions options) 285 throws IOException { 286 Uri resourceUri = options.getResourceUri(); 287 if (resourceUri != null) { 288 try { 289 InputStream bitmapStream = 290 options.getContext().getContentResolver().openInputStream(resourceUri); 291 292 if (bitmapStream != null) { 293 return decodeBitmap(bitmapStream, options); 294 } else { 295 Log.w(TAG, "Content provider returned a null InputStream when trying to " + 296 "open resource."); 297 return null; 298 } 299 } catch (FileNotFoundException e) { 300 Log.e(TAG, "FileNotFoundException during openInputStream for uri: " 301 + resourceUri.toString()); 302 return null; 303 } 304 } else { 305 Log.w(TAG, "Get null resourceUri from BitmapWorkerOptions."); 306 return null; 307 } 308 } 309 310 /** 311 * load drawable for non-bitmap resource or InputStream for bitmap resource without 312 * caching Bitmap in Resources. So that caller can maintain a different caching 313 * storage with less memory used. 314 * @return either {@link Drawable} for xml and ColorDrawable <br> 315 * or {@link InputStream} for Bitmap resource 316 */ 317 private static Object loadDrawable(Context context, ShortcutIconResource r) 318 throws NameNotFoundException { 319 Resources resources = context.getPackageManager() 320 .getResourcesForApplication(r.packageName); 321 if (resources == null) { 322 return null; 323 } 324 resources.updateConfiguration(context.getResources().getConfiguration(), 325 context.getResources().getDisplayMetrics()); 326 final int id = resources.getIdentifier(r.resourceName, null, null); 327 if (id == 0) { 328 Log.e(TAG, "Couldn't get resource " + r.resourceName + " in resources of " 329 + r.packageName); 330 return null; 331 } 332 TypedValue value = new TypedValue(); 333 resources.getValue(id, value, true); 334 if ((value.type == TypedValue.TYPE_STRING && value.string.toString().endsWith(".xml")) || ( 335 value.type >= TypedValue.TYPE_FIRST_COLOR_INT 336 && value.type <= TypedValue.TYPE_LAST_COLOR_INT)) { 337 return resources.getDrawable(id); 338 } 339 return resources.openRawResource(id, value); 340 } 341 342 public static Drawable getDrawable(Context context, ShortcutIconResource iconResource) 343 throws NameNotFoundException { 344 Resources resources = 345 context.getPackageManager().getResourcesForApplication(iconResource.packageName); 346 resources.updateConfiguration(context.getResources().getConfiguration(), 347 context.getResources().getDisplayMetrics()); 348 int id = resources.getIdentifier(iconResource.resourceName, null, null); 349 if (id == 0) { 350 throw new NameNotFoundException(); 351 } 352 return resources.getDrawable(id); 353 } 354 355 @SuppressWarnings("deprecation") 356 private Drawable getAccountImage(BitmapWorkerOptions options) { 357 String accountName = UriUtils.getAccountName(options.getResourceUri()); 358 Context context = options.getContext(); 359 360 if (accountName != null && context != null) { 361 Account thisAccount = null; 362 for (Account account : AccountManager.get(context). 363 getAccountsByType(GOOGLE_ACCOUNT_TYPE)) { 364 if (account.name.equals(accountName)) { 365 thisAccount = account; 366 break; 367 } 368 } 369 if (thisAccount != null) { 370 String picUriString = AccountImageHelper.getAccountPictureUri(context, thisAccount); 371 if (picUriString != null) { 372 BitmapWorkerOptions.Builder optionBuilder = 373 new BitmapWorkerOptions.Builder(context) 374 .width(options.getWidth()) 375 .height(options.getHeight()) 376 .cacheFlag(options.getCacheFlag()) 377 .bitmapConfig(options.getBitmapConfig()) 378 .resource(Uri.parse(picUriString)); 379 return DrawableDownloader.getInstance(context) 380 .loadBitmapBlocking(optionBuilder.build()); 381 } 382 return null; 383 } 384 } 385 return null; 386 } 387} 388