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