/* * Copyright 2015. Appsi Mobile * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.appsimobile.appsii.appwidget; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.Log; import com.appsimobile.appsii.R; import com.appsimobile.appsii.compat.AppWidgetManagerCompat; import java.lang.ref.SoftReference; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import javax.inject.Inject; public class WidgetPreviewLoader { private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f; final Context mContext; final Canvas mCanvas = new Canvas(); final int mAppIconSize; private final AppWidgetManagerCompat mManager; private final AppWidgetIconCache mAppWidgetIconCache; private final RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); private final RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); private final PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); private final PaintCache mDefaultAppWidgetPreviewPaint = new PaintCache(); private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); @Inject public WidgetPreviewLoader(Context context, AppWidgetIconCache appWidgetIconCache, AppWidgetManagerCompat appWidgetManagerCompat) { mContext = context.getApplicationContext(); mAppWidgetIconCache = appWidgetIconCache; mManager = appWidgetManagerCompat; mAppIconSize = (int) (80 * context.getResources().getDisplayMetrics().density); } private static void renderDrawableToBitmap( Drawable d, Bitmap bitmap, int x, int y, int w, int h) { if (bitmap != null) { Canvas c = new Canvas(bitmap); Rect oldBounds = d.copyBounds(); d.setBounds(x, y, x + w, y + h); d.draw(c); d.setBounds(oldBounds); // Restore the bounds c.setBitmap(null); } } public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, Bitmap preview) { int maxWidth = maxWidthForWidgetPreview(2); int maxHeight = maxHeightForWidgetPreview(2); return generateWidgetPreview(info, 2, 2, maxWidth, maxHeight, preview, null); } public int maxWidthForWidgetPreview(int spanX) { return (int) (60 * spanX * mContext.getResources().getDisplayMetrics().density); } public int maxHeightForWidgetPreview(int spanY) { return (int) (60 * spanY * mContext.getResources().getDisplayMetrics().density); } public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, int cellHSpan, int cellVSpan, int maxPreviewWidth, int maxPreviewHeight, Bitmap preview, int[] preScaledWidthOut) { if (info == null) return null; // Load the preview image if possible if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; Drawable drawable = null; if (info.previewImage != 0) { drawable = mManager.loadPreview(info); if (drawable != null) { drawable = mutateOnMainThread(drawable); } else { Log.w("WidgetPreview", "Can't load widget preview drawable 0x" + Integer.toHexString(info.previewImage) + " for provider: " + info.provider); } } int previewWidth; int previewHeight; Bitmap defaultPreview = null; boolean widgetPreviewExists = (drawable != null); if (widgetPreviewExists) { previewWidth = drawable.getIntrinsicWidth(); previewHeight = drawable.getIntrinsicHeight(); } else { // Generate a preview image if we couldn't load one if (cellHSpan < 1) cellHSpan = 1; if (cellVSpan < 1) cellVSpan = 1; // This Drawable is not directly drawn, so there's no need to mutate it. BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() .getDrawable(R.drawable.widget_tile); final int previewDrawableWidth = previewDrawable .getIntrinsicWidth(); final int previewDrawableHeight = previewDrawable .getIntrinsicHeight(); previewWidth = previewDrawableWidth * cellHSpan; previewHeight = previewDrawableHeight * cellVSpan; defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); final Canvas c = mCanvas; int canvasState = c.save(); c.setBitmap(defaultPreview); Paint p = mDefaultAppWidgetPreviewPaint.get(); if (p == null) { p = new Paint(); p.setShader(new BitmapShader(previewDrawable.getBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); mDefaultAppWidgetPreviewPaint.set(p); } final Rect dest = mCachedAppWidgetPreviewDestRect.get(); dest.set(0, 0, previewWidth, previewHeight); c.drawRect(dest, p); c.setBitmap(null); c.restoreToCount(canvasState); // Draw the icon in the top left corner int minOffset = (int) (mAppIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); int smallestSide = Math.min(previewWidth, previewHeight); float iconScale = Math.min((float) smallestSide / (mAppIconSize + 2 * minOffset), 1f); try { Drawable icon = mManager.loadIcon(info, mAppWidgetIconCache); if (icon != null) { int hoffset = (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); int yoffset = (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); icon = mutateOnMainThread(icon); renderDrawableToBitmap(icon, defaultPreview, hoffset, yoffset, (int) (mAppIconSize * iconScale), (int) (mAppIconSize * iconScale)); } } catch (Resources.NotFoundException ignore) { } } // Scale to fit width only - let the widget preview be clipped in the // vertical dimension float scale = 1f; if (preScaledWidthOut != null) { preScaledWidthOut[0] = previewWidth; } if (previewWidth > maxPreviewWidth) { scale = maxPreviewWidth / (float) previewWidth; } if (scale != 1f) { previewWidth = (int) (scale * previewWidth); previewHeight = (int) (scale * previewHeight); } // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size if (preview == null) { preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); } // Draw the scaled preview into the final bitmap int x = (preview.getWidth() - previewWidth) / 2; if (widgetPreviewExists) { renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, previewHeight); } else { final Canvas c = mCanvas; int canvasState = c.save(); final Rect src = mCachedAppWidgetPreviewSrcRect.get(); final Rect dest = mCachedAppWidgetPreviewDestRect.get(); c.setBitmap(preview); src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); dest.set(x, 0, x + previewWidth, previewHeight); Paint p = mCachedAppWidgetPreviewPaint.get(); if (p == null) { p = new Paint(); p.setFilterBitmap(true); mCachedAppWidgetPreviewPaint.set(p); } c.drawBitmap(defaultPreview, src, dest, p); c.setBitmap(null); c.restoreToCount(canvasState); } return mManager.getBadgeBitmap(info, preview); } private Drawable mutateOnMainThread(final Drawable drawable) { try { return mMainThreadExecutor.submit(new Callable<Drawable>() { @Override public Drawable call() throws Exception { return drawable.mutate(); } }).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } catch (ExecutionException e) { throw new RuntimeException(e); } } private static abstract class SoftReferenceThreadLocal<T> { private final ThreadLocal<SoftReference<T>> mThreadLocal; public SoftReferenceThreadLocal() { mThreadLocal = new ThreadLocal<>(); } public void set(T t) { mThreadLocal.set(new SoftReference<>(t)); } public T get() { SoftReference<T> reference = mThreadLocal.get(); T obj; if (reference == null) { obj = initialValue(); mThreadLocal.set(new SoftReference<>(obj)); return obj; } else { obj = reference.get(); if (obj == null) { obj = initialValue(); mThreadLocal.set(new SoftReference<>(obj)); } return obj; } } abstract T initialValue(); } private static class PaintCache extends SoftReferenceThreadLocal<Paint> { PaintCache() { } @Override protected Paint initialValue() { return null; } } private static class RectCache extends SoftReferenceThreadLocal<Rect> { RectCache() { } @Override protected Rect initialValue() { return new Rect(); } } }