1package com.android.launcher3; 2 3import android.appwidget.AppWidgetProviderInfo; 4import android.content.ComponentName; 5import android.content.ContentValues; 6import android.content.Context; 7import android.content.pm.PackageManager; 8import android.content.pm.ResolveInfo; 9import android.content.res.Resources; 10import android.database.Cursor; 11import android.database.sqlite.SQLiteDatabase; 12import android.database.sqlite.SQLiteOpenHelper; 13import android.graphics.Bitmap; 14import android.graphics.Bitmap.Config; 15import android.graphics.BitmapFactory; 16import android.graphics.Canvas; 17import android.graphics.ColorMatrix; 18import android.graphics.ColorMatrixColorFilter; 19import android.graphics.Paint; 20import android.graphics.PorterDuff; 21import android.graphics.Rect; 22import android.graphics.Shader; 23import android.graphics.drawable.BitmapDrawable; 24import android.graphics.drawable.Drawable; 25import android.os.AsyncTask; 26import android.util.Log; 27 28import java.io.ByteArrayOutputStream; 29import java.io.File; 30import java.lang.ref.SoftReference; 31import java.lang.ref.WeakReference; 32import java.util.ArrayList; 33import java.util.HashMap; 34import java.util.HashSet; 35 36abstract class SoftReferenceThreadLocal<T> { 37 private ThreadLocal<SoftReference<T>> mThreadLocal; 38 public SoftReferenceThreadLocal() { 39 mThreadLocal = new ThreadLocal<SoftReference<T>>(); 40 } 41 42 abstract T initialValue(); 43 44 public void set(T t) { 45 mThreadLocal.set(new SoftReference<T>(t)); 46 } 47 48 public T get() { 49 SoftReference<T> reference = mThreadLocal.get(); 50 T obj; 51 if (reference == null) { 52 obj = initialValue(); 53 mThreadLocal.set(new SoftReference<T>(obj)); 54 return obj; 55 } else { 56 obj = reference.get(); 57 if (obj == null) { 58 obj = initialValue(); 59 mThreadLocal.set(new SoftReference<T>(obj)); 60 } 61 return obj; 62 } 63 } 64} 65 66class CanvasCache extends SoftReferenceThreadLocal<Canvas> { 67 @Override 68 protected Canvas initialValue() { 69 return new Canvas(); 70 } 71} 72 73class PaintCache extends SoftReferenceThreadLocal<Paint> { 74 @Override 75 protected Paint initialValue() { 76 return null; 77 } 78} 79 80class BitmapCache extends SoftReferenceThreadLocal<Bitmap> { 81 @Override 82 protected Bitmap initialValue() { 83 return null; 84 } 85} 86 87class RectCache extends SoftReferenceThreadLocal<Rect> { 88 @Override 89 protected Rect initialValue() { 90 return new Rect(); 91 } 92} 93 94class BitmapFactoryOptionsCache extends SoftReferenceThreadLocal<BitmapFactory.Options> { 95 @Override 96 protected BitmapFactory.Options initialValue() { 97 return new BitmapFactory.Options(); 98 } 99} 100 101public class WidgetPreviewLoader { 102 static final String TAG = "WidgetPreviewLoader"; 103 104 private int mPreviewBitmapWidth; 105 private int mPreviewBitmapHeight; 106 private String mSize; 107 private Context mContext; 108 private PackageManager mPackageManager; 109 private PagedViewCellLayout mWidgetSpacingLayout; 110 111 // Used for drawing shortcut previews 112 private BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); 113 private PaintCache mCachedShortcutPreviewPaint = new PaintCache(); 114 private CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); 115 116 // Used for drawing widget previews 117 private CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); 118 private RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); 119 private RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); 120 private PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); 121 private String mCachedSelectQuery; 122 private BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache(); 123 124 private int mAppIconSize; 125 private IconCache mIconCache; 126 127 private final float sWidgetPreviewIconPaddingPercentage = 0.25f; 128 129 private CacheDb mDb; 130 131 private HashMap<String, WeakReference<Bitmap>> mLoadedPreviews; 132 private ArrayList<SoftReference<Bitmap>> mUnusedBitmaps; 133 private static HashSet<String> sInvalidPackages; 134 135 static { 136 sInvalidPackages = new HashSet<String>(); 137 } 138 139 public WidgetPreviewLoader(Context context) { 140 LauncherAppState app = LauncherAppState.getInstance(); 141 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); 142 143 mContext = context; 144 mPackageManager = mContext.getPackageManager(); 145 mAppIconSize = grid.iconSizePx; 146 mIconCache = app.getIconCache(); 147 mDb = app.getWidgetPreviewCacheDb(); 148 mLoadedPreviews = new HashMap<String, WeakReference<Bitmap>>(); 149 mUnusedBitmaps = new ArrayList<SoftReference<Bitmap>>(); 150 } 151 152 public void setPreviewSize(int previewWidth, int previewHeight, 153 PagedViewCellLayout widgetSpacingLayout) { 154 mPreviewBitmapWidth = previewWidth; 155 mPreviewBitmapHeight = previewHeight; 156 mSize = previewWidth + "x" + previewHeight; 157 mWidgetSpacingLayout = widgetSpacingLayout; 158 } 159 160 public Bitmap getPreview(final Object o) { 161 final String name = getObjectName(o); 162 final String packageName = getObjectPackage(o); 163 // check if the package is valid 164 boolean packageValid = true; 165 synchronized(sInvalidPackages) { 166 packageValid = !sInvalidPackages.contains(packageName); 167 } 168 if (!packageValid) { 169 return null; 170 } 171 if (packageValid) { 172 synchronized(mLoadedPreviews) { 173 // check if it exists in our existing cache 174 if (mLoadedPreviews.containsKey(name) && mLoadedPreviews.get(name).get() != null) { 175 return mLoadedPreviews.get(name).get(); 176 } 177 } 178 } 179 180 Bitmap unusedBitmap = null; 181 synchronized(mUnusedBitmaps) { 182 // not in cache; we need to load it from the db 183 while ((unusedBitmap == null || !unusedBitmap.isMutable() || 184 unusedBitmap.getWidth() != mPreviewBitmapWidth || 185 unusedBitmap.getHeight() != mPreviewBitmapHeight) 186 && mUnusedBitmaps.size() > 0) { 187 unusedBitmap = mUnusedBitmaps.remove(0).get(); 188 } 189 if (unusedBitmap != null) { 190 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 191 c.setBitmap(unusedBitmap); 192 c.drawColor(0, PorterDuff.Mode.CLEAR); 193 c.setBitmap(null); 194 } 195 } 196 197 if (unusedBitmap == null) { 198 unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight, 199 Bitmap.Config.ARGB_8888); 200 } 201 202 Bitmap preview = null; 203 204 if (packageValid) { 205 preview = readFromDb(name, unusedBitmap); 206 } 207 208 if (preview != null) { 209 synchronized(mLoadedPreviews) { 210 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); 211 } 212 return preview; 213 } else { 214 // it's not in the db... we need to generate it 215 final Bitmap generatedPreview = generatePreview(o, unusedBitmap); 216 preview = generatedPreview; 217 if (preview != unusedBitmap) { 218 throw new RuntimeException("generatePreview is not recycling the bitmap " + o); 219 } 220 221 synchronized(mLoadedPreviews) { 222 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); 223 } 224 225 // write to db on a thread pool... this can be done lazily and improves the performance 226 // of the first time widget previews are loaded 227 new AsyncTask<Void, Void, Void>() { 228 public Void doInBackground(Void ... args) { 229 writeToDb(o, generatedPreview); 230 return null; 231 } 232 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 233 234 return preview; 235 } 236 } 237 238 public void recycleBitmap(Object o, Bitmap bitmapToRecycle) { 239 String name = getObjectName(o); 240 synchronized (mLoadedPreviews) { 241 if (mLoadedPreviews.containsKey(name)) { 242 Bitmap b = mLoadedPreviews.get(name).get(); 243 if (b == bitmapToRecycle) { 244 mLoadedPreviews.remove(name); 245 if (bitmapToRecycle.isMutable()) { 246 synchronized (mUnusedBitmaps) { 247 mUnusedBitmaps.add(new SoftReference<Bitmap>(b)); 248 } 249 } 250 } else { 251 throw new RuntimeException("Bitmap passed in doesn't match up"); 252 } 253 } 254 } 255 } 256 257 static class CacheDb extends SQLiteOpenHelper { 258 final static int DB_VERSION = 2; 259 final static String DB_NAME = "widgetpreviews.db"; 260 final static String TABLE_NAME = "shortcut_and_widget_previews"; 261 final static String COLUMN_NAME = "name"; 262 final static String COLUMN_SIZE = "size"; 263 final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; 264 Context mContext; 265 266 public CacheDb(Context context) { 267 super(context, new File(context.getCacheDir(), DB_NAME).getPath(), null, DB_VERSION); 268 // Store the context for later use 269 mContext = context; 270 } 271 272 @Override 273 public void onCreate(SQLiteDatabase database) { 274 database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + 275 COLUMN_NAME + " TEXT NOT NULL, " + 276 COLUMN_SIZE + " TEXT NOT NULL, " + 277 COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " + 278 "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " + 279 ");"); 280 } 281 282 @Override 283 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 284 if (oldVersion != newVersion) { 285 // Delete all the records; they'll be repopulated as this is a cache 286 db.execSQL("DELETE FROM " + TABLE_NAME); 287 } 288 } 289 } 290 291 private static final String WIDGET_PREFIX = "Widget:"; 292 private static final String SHORTCUT_PREFIX = "Shortcut:"; 293 294 private static String getObjectName(Object o) { 295 // should cache the string builder 296 StringBuilder sb = new StringBuilder(); 297 String output; 298 if (o instanceof AppWidgetProviderInfo) { 299 sb.append(WIDGET_PREFIX); 300 sb.append(((AppWidgetProviderInfo) o).provider.flattenToString()); 301 output = sb.toString(); 302 sb.setLength(0); 303 } else { 304 sb.append(SHORTCUT_PREFIX); 305 306 ResolveInfo info = (ResolveInfo) o; 307 sb.append(new ComponentName(info.activityInfo.packageName, 308 info.activityInfo.name).flattenToString()); 309 output = sb.toString(); 310 sb.setLength(0); 311 } 312 return output; 313 } 314 315 private String getObjectPackage(Object o) { 316 if (o instanceof AppWidgetProviderInfo) { 317 return ((AppWidgetProviderInfo) o).provider.getPackageName(); 318 } else { 319 ResolveInfo info = (ResolveInfo) o; 320 return info.activityInfo.packageName; 321 } 322 } 323 324 private void writeToDb(Object o, Bitmap preview) { 325 String name = getObjectName(o); 326 SQLiteDatabase db = mDb.getWritableDatabase(); 327 ContentValues values = new ContentValues(); 328 329 values.put(CacheDb.COLUMN_NAME, name); 330 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 331 preview.compress(Bitmap.CompressFormat.PNG, 100, stream); 332 values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray()); 333 values.put(CacheDb.COLUMN_SIZE, mSize); 334 db.insert(CacheDb.TABLE_NAME, null, values); 335 } 336 337 public static void removePackageFromDb(final CacheDb cacheDb, final String packageName) { 338 synchronized(sInvalidPackages) { 339 sInvalidPackages.add(packageName); 340 } 341 new AsyncTask<Void, Void, Void>() { 342 public Void doInBackground(Void ... args) { 343 SQLiteDatabase db = cacheDb.getWritableDatabase(); 344 db.delete(CacheDb.TABLE_NAME, 345 CacheDb.COLUMN_NAME + " LIKE ? OR " + 346 CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query 347 new String[] { 348 WIDGET_PREFIX + packageName + "/%", 349 SHORTCUT_PREFIX + packageName + "/%"} // args to SELECT query 350 ); 351 synchronized(sInvalidPackages) { 352 sInvalidPackages.remove(packageName); 353 } 354 return null; 355 } 356 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 357 } 358 359 public static void removeItemFromDb(final CacheDb cacheDb, final String objectName) { 360 new AsyncTask<Void, Void, Void>() { 361 public Void doInBackground(Void ... args) { 362 SQLiteDatabase db = cacheDb.getWritableDatabase(); 363 db.delete(CacheDb.TABLE_NAME, 364 CacheDb.COLUMN_NAME + " = ? ", // SELECT query 365 new String[] { objectName }); // args to SELECT query 366 return null; 367 } 368 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 369 } 370 371 private Bitmap readFromDb(String name, Bitmap b) { 372 if (mCachedSelectQuery == null) { 373 mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " + 374 CacheDb.COLUMN_SIZE + " = ?"; 375 } 376 SQLiteDatabase db = mDb.getReadableDatabase(); 377 Cursor result = db.query(CacheDb.TABLE_NAME, 378 new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return 379 mCachedSelectQuery, // select query 380 new String[] { name, mSize }, // args to select query 381 null, 382 null, 383 null, 384 null); 385 if (result.getCount() > 0) { 386 result.moveToFirst(); 387 byte[] blob = result.getBlob(0); 388 result.close(); 389 final BitmapFactory.Options opts = mCachedBitmapFactoryOptions.get(); 390 opts.inBitmap = b; 391 opts.inSampleSize = 1; 392 try { 393 return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); 394 } catch (IllegalArgumentException e) { 395 removeItemFromDb(mDb, name); 396 return null; 397 } 398 } else { 399 result.close(); 400 return null; 401 } 402 } 403 404 public Bitmap generatePreview(Object info, Bitmap preview) { 405 if (preview != null && 406 (preview.getWidth() != mPreviewBitmapWidth || 407 preview.getHeight() != mPreviewBitmapHeight)) { 408 throw new RuntimeException("Improperly sized bitmap passed as argument"); 409 } 410 if (info instanceof AppWidgetProviderInfo) { 411 return generateWidgetPreview((AppWidgetProviderInfo) info, preview); 412 } else { 413 return generateShortcutPreview( 414 (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview); 415 } 416 } 417 418 public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, Bitmap preview) { 419 int[] cellSpans = Launcher.getSpanForWidget(mContext, info); 420 int maxWidth = maxWidthForWidgetPreview(cellSpans[0]); 421 int maxHeight = maxHeightForWidgetPreview(cellSpans[1]); 422 return generateWidgetPreview(info.provider, info.previewImage, info.icon, 423 cellSpans[0], cellSpans[1], maxWidth, maxHeight, preview, null); 424 } 425 426 public int maxWidthForWidgetPreview(int spanX) { 427 return Math.min(mPreviewBitmapWidth, 428 mWidgetSpacingLayout.estimateCellWidth(spanX)); 429 } 430 431 public int maxHeightForWidgetPreview(int spanY) { 432 return Math.min(mPreviewBitmapHeight, 433 mWidgetSpacingLayout.estimateCellHeight(spanY)); 434 } 435 436 public Bitmap generateWidgetPreview(ComponentName provider, int previewImage, 437 int iconId, int cellHSpan, int cellVSpan, int maxPreviewWidth, int maxPreviewHeight, 438 Bitmap preview, int[] preScaledWidthOut) { 439 // Load the preview image if possible 440 String packageName = provider.getPackageName(); 441 if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; 442 if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; 443 444 Drawable drawable = null; 445 if (previewImage != 0) { 446 drawable = mPackageManager.getDrawable(packageName, previewImage, null); 447 if (drawable == null) { 448 Log.w(TAG, "Can't load widget preview drawable 0x" + 449 Integer.toHexString(previewImage) + " for provider: " + provider); 450 } 451 } 452 453 int previewWidth; 454 int previewHeight; 455 Bitmap defaultPreview = null; 456 boolean widgetPreviewExists = (drawable != null); 457 if (widgetPreviewExists) { 458 previewWidth = drawable.getIntrinsicWidth(); 459 previewHeight = drawable.getIntrinsicHeight(); 460 } else { 461 // Generate a preview image if we couldn't load one 462 if (cellHSpan < 1) cellHSpan = 1; 463 if (cellVSpan < 1) cellVSpan = 1; 464 465 BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() 466 .getDrawable(R.drawable.widget_tile); 467 final int previewDrawableWidth = previewDrawable 468 .getIntrinsicWidth(); 469 final int previewDrawableHeight = previewDrawable 470 .getIntrinsicHeight(); 471 previewWidth = previewDrawableWidth * cellHSpan; 472 previewHeight = previewDrawableHeight * cellVSpan; 473 474 defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, 475 Config.ARGB_8888); 476 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 477 c.setBitmap(defaultPreview); 478 previewDrawable.setBounds(0, 0, previewWidth, previewHeight); 479 previewDrawable.setTileModeXY(Shader.TileMode.REPEAT, 480 Shader.TileMode.REPEAT); 481 previewDrawable.draw(c); 482 c.setBitmap(null); 483 484 // Draw the icon in the top left corner 485 int minOffset = (int) (mAppIconSize * sWidgetPreviewIconPaddingPercentage); 486 int smallestSide = Math.min(previewWidth, previewHeight); 487 float iconScale = Math.min((float) smallestSide 488 / (mAppIconSize + 2 * minOffset), 1f); 489 490 try { 491 Drawable icon = null; 492 int hoffset = 493 (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); 494 int yoffset = 495 (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); 496 if (iconId > 0) 497 icon = mIconCache.getFullResIcon(packageName, iconId); 498 if (icon != null) { 499 renderDrawableToBitmap(icon, defaultPreview, hoffset, 500 yoffset, (int) (mAppIconSize * iconScale), 501 (int) (mAppIconSize * iconScale)); 502 } 503 } catch (Resources.NotFoundException e) { 504 } 505 } 506 507 // Scale to fit width only - let the widget preview be clipped in the 508 // vertical dimension 509 float scale = 1f; 510 if (preScaledWidthOut != null) { 511 preScaledWidthOut[0] = previewWidth; 512 } 513 if (previewWidth > maxPreviewWidth) { 514 scale = maxPreviewWidth / (float) previewWidth; 515 } 516 if (scale != 1f) { 517 previewWidth = (int) (scale * previewWidth); 518 previewHeight = (int) (scale * previewHeight); 519 } 520 521 // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size 522 if (preview == null) { 523 preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); 524 } 525 526 // Draw the scaled preview into the final bitmap 527 int x = (preview.getWidth() - previewWidth) / 2; 528 if (widgetPreviewExists) { 529 renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, 530 previewHeight); 531 } else { 532 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 533 final Rect src = mCachedAppWidgetPreviewSrcRect.get(); 534 final Rect dest = mCachedAppWidgetPreviewDestRect.get(); 535 c.setBitmap(preview); 536 src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); 537 dest.set(x, 0, x + previewWidth, previewHeight); 538 539 Paint p = mCachedAppWidgetPreviewPaint.get(); 540 if (p == null) { 541 p = new Paint(); 542 p.setFilterBitmap(true); 543 mCachedAppWidgetPreviewPaint.set(p); 544 } 545 c.drawBitmap(defaultPreview, src, dest, p); 546 c.setBitmap(null); 547 } 548 return preview; 549 } 550 551 private Bitmap generateShortcutPreview( 552 ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { 553 Bitmap tempBitmap = mCachedShortcutPreviewBitmap.get(); 554 final Canvas c = mCachedShortcutPreviewCanvas.get(); 555 if (tempBitmap == null || 556 tempBitmap.getWidth() != maxWidth || 557 tempBitmap.getHeight() != maxHeight) { 558 tempBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); 559 mCachedShortcutPreviewBitmap.set(tempBitmap); 560 } else { 561 c.setBitmap(tempBitmap); 562 c.drawColor(0, PorterDuff.Mode.CLEAR); 563 c.setBitmap(null); 564 } 565 // Render the icon 566 Drawable icon = mIconCache.getFullResIcon(info); 567 568 int paddingTop = mContext. 569 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); 570 int paddingLeft = mContext. 571 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); 572 int paddingRight = mContext. 573 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); 574 575 int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); 576 577 renderDrawableToBitmap( 578 icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth); 579 580 if (preview != null && 581 (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight)) { 582 throw new RuntimeException("Improperly sized bitmap passed as argument"); 583 } else if (preview == null) { 584 preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); 585 } 586 587 c.setBitmap(preview); 588 // Draw a desaturated/scaled version of the icon in the background as a watermark 589 Paint p = mCachedShortcutPreviewPaint.get(); 590 if (p == null) { 591 p = new Paint(); 592 ColorMatrix colorMatrix = new ColorMatrix(); 593 colorMatrix.setSaturation(0); 594 p.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); 595 p.setAlpha((int) (255 * 0.06f)); 596 mCachedShortcutPreviewPaint.set(p); 597 } 598 c.drawBitmap(tempBitmap, 0, 0, p); 599 c.setBitmap(null); 600 601 renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize); 602 603 return preview; 604 } 605 606 607 public static void renderDrawableToBitmap( 608 Drawable d, Bitmap bitmap, int x, int y, int w, int h) { 609 renderDrawableToBitmap(d, bitmap, x, y, w, h, 1f); 610 } 611 612 private static void renderDrawableToBitmap( 613 Drawable d, Bitmap bitmap, int x, int y, int w, int h, 614 float scale) { 615 if (bitmap != null) { 616 Canvas c = new Canvas(bitmap); 617 c.scale(scale, scale); 618 Rect oldBounds = d.copyBounds(); 619 d.setBounds(x, y, x + w, y + h); 620 d.draw(c); 621 d.setBounds(oldBounds); // Restore the bounds 622 c.setBitmap(null); 623 } 624 } 625 626} 627