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