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