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 final WeakReference<ImageView> mImageView; 67 private int mOriginalWidth; 68 private int mOriginalHeight; 69 private final RecycleBitmapPool mRecycledBitmaps; 70 71 private final 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); 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 final ImageView imageView = mImageView.get(); 137 if (imageView != null) { 138 imageView.setImageDrawable(bitmap); 139 } 140 } 141 142 @Override onCancelled(Drawable result)143 protected void onCancelled(Drawable result) { 144 if (result instanceof RefcountBitmapDrawable) { 145 // Remove the extra refcount created by us, DrawableDownloader LruCache 146 // still holds one to the bitmap 147 RefcountBitmapDrawable d = (RefcountBitmapDrawable) result; 148 d.getRefcountObject().releaseRef(); 149 } 150 } 151 getBitmapFromResource(ShortcutIconResource iconResource, BitmapWorkerOptions outputOptions)152 private Drawable getBitmapFromResource(ShortcutIconResource iconResource, 153 BitmapWorkerOptions outputOptions) throws IOException { 154 if (DEBUG) { 155 Log.d(TAG, "Loading " + iconResource.toString()); 156 } 157 try { 158 Object drawable = loadDrawable(outputOptions.getContext(), iconResource); 159 if (drawable instanceof InputStream) { 160 // Most of these are bitmaps, so resize properly. 161 return decodeBitmap((InputStream)drawable, outputOptions); 162 } else if (drawable instanceof Drawable){ 163 Drawable d = (Drawable) drawable; 164 mOriginalWidth = d.getIntrinsicWidth(); 165 mOriginalHeight = d.getIntrinsicHeight(); 166 return d; 167 } else { 168 Log.w(TAG, "getBitmapFromResource failed, unrecognized resource: " + drawable); 169 return null; 170 } 171 } catch (NameNotFoundException e) { 172 Log.w(TAG, "Could not load package: " + iconResource.packageName + "! NameNotFound"); 173 return null; 174 } catch (NotFoundException e) { 175 Log.w(TAG, "Could not load resource: " + iconResource.resourceName + "! NotFound"); 176 return null; 177 } 178 } 179 decodeBitmap(InputStream in, BitmapWorkerOptions options)180 private Drawable decodeBitmap(InputStream in, BitmapWorkerOptions options) 181 throws IOException { 182 CachedInputStream bufferedStream = null; 183 BitmapFactory.Options bitmapOptions = null; 184 try { 185 bufferedStream = new CachedInputStream(in); 186 // Let the bufferedStream be able to mark unlimited bytes up to full stream length. 187 // The value that BitmapFactory uses (1024) is too small for detecting bounds 188 bufferedStream.setOverrideMarkLimit(Integer.MAX_VALUE); 189 bitmapOptions = new BitmapFactory.Options(); 190 bitmapOptions.inJustDecodeBounds = true; 191 if (options.getBitmapConfig() != null) { 192 bitmapOptions.inPreferredConfig = options.getBitmapConfig(); 193 } 194 bitmapOptions.inTempStorage = ByteArrayPool.get16KBPool().allocateChunk(); 195 bufferedStream.mark(Integer.MAX_VALUE); 196 BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions); 197 198 mOriginalWidth = bitmapOptions.outWidth; 199 mOriginalHeight = bitmapOptions.outHeight; 200 int heightScale = 1; 201 int height = options.getHeight(); 202 if (height > 0) { 203 heightScale = bitmapOptions.outHeight / height; 204 } 205 206 int widthScale = 1; 207 int width = options.getWidth(); 208 if (width > 0) { 209 widthScale = bitmapOptions.outWidth / width; 210 } 211 212 int scale = heightScale > widthScale ? heightScale : widthScale; 213 if (scale <= 1) { 214 scale = 1; 215 } else { 216 int shift = 0; 217 do { 218 scale >>= 1; 219 shift++; 220 } while (scale != 0); 221 scale = 1 << (shift - 1); 222 } 223 224 if (DEBUG) { 225 Log.d("BitmapWorkerTask", "Source bitmap: (" + bitmapOptions.outWidth + "x" 226 + bitmapOptions.outHeight + "). Max size: (" + options.getWidth() + "x" 227 + options.getHeight() + "). Chosen scale: " + scale + " -> " + scale); 228 } 229 230 // Reset buffer to original position and disable the overrideMarkLimit 231 bufferedStream.reset(); 232 bufferedStream.setOverrideMarkLimit(0); 233 Bitmap bitmap; 234 try { 235 bitmapOptions.inJustDecodeBounds = false; 236 bitmapOptions.inSampleSize = scale; 237 bitmapOptions.inMutable = true; 238 bitmapOptions.inBitmap = mRecycledBitmaps.getRecycledBitmap( 239 mOriginalWidth / scale, mOriginalHeight / scale); 240 bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions); 241 } catch (RuntimeException ex) { 242 Log.e(TAG, "RuntimeException" + ex + ", trying decodeStream again"); 243 bufferedStream.reset(); 244 bufferedStream.setOverrideMarkLimit(0); 245 bitmapOptions.inBitmap = null; 246 bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions); 247 } 248 if (bitmap == null) { 249 Log.d(TAG, "bitmap was null"); 250 return null; 251 } 252 RefcountObject<Bitmap> object = new RefcountObject<>(bitmap); 253 object.addRef(); 254 object.setRefcountListener(mRefcountListener); 255 RefcountBitmapDrawable d = new RefcountBitmapDrawable( 256 options.getContext().getResources(), object); 257 return d; 258 } finally { 259 Log.w(TAG, "couldn't load bitmap, releasing resources"); 260 if (bitmapOptions != null) { 261 ByteArrayPool.get16KBPool().releaseChunk(bitmapOptions.inTempStorage); 262 } 263 if (bufferedStream != null) { 264 bufferedStream.close(); 265 } 266 } 267 } 268 getBitmapFromHttp(BitmapWorkerOptions options)269 private Drawable getBitmapFromHttp(BitmapWorkerOptions options) throws IOException { 270 URL url = new URL(options.getResourceUri().toString()); 271 if (DEBUG) { 272 Log.d(TAG, "Loading " + url); 273 } 274 try { 275 // TODO use volley for better disk cache 276 URLConnection connection = url.openConnection(); 277 connection.setConnectTimeout(SOCKET_TIMEOUT); 278 connection.setReadTimeout(READ_TIMEOUT); 279 InputStream in = connection.getInputStream(); 280 return decodeBitmap(in, options); 281 } catch (SocketTimeoutException e) { 282 Log.e(TAG, "loading " + url + " timed out"); 283 } 284 return null; 285 } 286 getBitmapFromContent(BitmapWorkerOptions options)287 private Drawable getBitmapFromContent(BitmapWorkerOptions options) 288 throws IOException { 289 Uri resourceUri = options.getResourceUri(); 290 if (resourceUri != null) { 291 try { 292 InputStream bitmapStream = 293 options.getContext().getContentResolver().openInputStream(resourceUri); 294 295 if (bitmapStream != null) { 296 return decodeBitmap(bitmapStream, options); 297 } else { 298 Log.w(TAG, "Content provider returned a null InputStream when trying to " + 299 "open resource."); 300 return null; 301 } 302 } catch (FileNotFoundException e) { 303 Log.e(TAG, "FileNotFoundException during openInputStream for uri: " 304 + resourceUri.toString()); 305 return null; 306 } 307 } else { 308 Log.w(TAG, "Get null resourceUri from BitmapWorkerOptions."); 309 return null; 310 } 311 } 312 313 /** 314 * load drawable for non-bitmap resource or InputStream for bitmap resource without 315 * caching Bitmap in Resources. So that caller can maintain a different caching 316 * storage with less memory used. 317 * @return either {@link Drawable} for xml and ColorDrawable <br> 318 * or {@link InputStream} for Bitmap resource 319 */ loadDrawable(Context context, ShortcutIconResource r)320 private static Object loadDrawable(Context context, ShortcutIconResource r) 321 throws NameNotFoundException { 322 Resources resources = context.getPackageManager().getResourcesForApplication(r.packageName); 323 if (resources == null) { 324 return null; 325 } 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 getDrawable(Context context, ShortcutIconResource iconResource)342 public static Drawable getDrawable(Context context, ShortcutIconResource iconResource) 343 throws NameNotFoundException { 344 Resources resources = 345 context.getPackageManager().getResourcesForApplication(iconResource.packageName); 346 int id = resources.getIdentifier(iconResource.resourceName, null, null); 347 if (id == 0) { 348 throw new NameNotFoundException(); 349 } 350 return resources.getDrawable(id); 351 } 352 getAccountImage(BitmapWorkerOptions options)353 private Drawable getAccountImage(BitmapWorkerOptions options) { 354 String accountName = UriUtils.getAccountName(options.getResourceUri()); 355 Context context = options.getContext(); 356 357 if (accountName != null && context != null) { 358 Account thisAccount = null; 359 for (Account account : AccountManager.get(context). 360 getAccountsByType(GOOGLE_ACCOUNT_TYPE)) { 361 if (account.name.equals(accountName)) { 362 thisAccount = account; 363 break; 364 } 365 } 366 if (thisAccount != null) { 367 String picUriString = AccountImageHelper.getAccountPictureUri(context, thisAccount); 368 if (picUriString != null) { 369 BitmapWorkerOptions.Builder optionBuilder = 370 new BitmapWorkerOptions.Builder(context) 371 .width(options.getWidth()) 372 .height(options.getHeight()) 373 .cacheFlag(options.getCacheFlag()) 374 .bitmapConfig(options.getBitmapConfig()) 375 .resource(Uri.parse(picUriString)); 376 return DrawableDownloader.getInstance(context) 377 .loadBitmapBlocking(optionBuilder.build()); 378 } 379 return null; 380 } 381 } 382 return null; 383 } 384 } 385