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