1 /* 2 * Copyright (C) 2017 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 package com.android.wallpaper.asset; 17 18 import android.app.Activity; 19 import android.content.Context; 20 import android.content.res.AssetFileDescriptor; 21 import android.graphics.Bitmap; 22 import android.graphics.BitmapFactory; 23 import android.graphics.Canvas; 24 import android.graphics.Point; 25 import android.graphics.Rect; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.graphics.drawable.ColorDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.graphics.drawable.LayerDrawable; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.util.Log; 34 import android.widget.ImageView; 35 36 import androidx.annotation.WorkerThread; 37 38 import com.android.wallpaper.module.DrawableLayerResolver; 39 import com.android.wallpaper.module.InjectorProvider; 40 41 import com.bumptech.glide.Glide; 42 import com.bumptech.glide.load.Key; 43 import com.bumptech.glide.load.MultiTransformation; 44 import com.bumptech.glide.load.Transformation; 45 import com.bumptech.glide.load.engine.DiskCacheStrategy; 46 import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; 47 import com.bumptech.glide.load.resource.bitmap.FitCenter; 48 import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; 49 import com.bumptech.glide.request.RequestOptions; 50 51 import java.io.IOException; 52 import java.security.MessageDigest; 53 import java.util.concurrent.ExecutionException; 54 import java.util.concurrent.ExecutorService; 55 import java.util.concurrent.Executors; 56 import java.util.concurrent.TimeUnit; 57 import java.util.concurrent.TimeoutException; 58 59 /** 60 * Asset wrapping a drawable for a live wallpaper thumbnail. 61 */ 62 public class LiveWallpaperThumbAsset extends Asset { 63 private static final String TAG = "LiveWallpaperThumbAsset"; 64 private static final ExecutorService sExecutorService = Executors.newCachedThreadPool(); 65 private static final int LOW_RES_THUMB_TIMEOUT_SECONDS = 2; 66 67 protected final Context mContext; 68 protected final android.app.WallpaperInfo mInfo; 69 protected final DrawableLayerResolver mLayerResolver; 70 // The content Uri of thumbnail 71 protected Uri mUri; 72 protected boolean mShouldCacheThumbnail; 73 private Drawable mCachedThumbnail; 74 LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info)75 public LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info) { 76 this(context, info, /* uri= */ null); 77 } 78 LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info, Uri uri)79 public LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info, Uri uri) { 80 this(context, info, uri, /* shouldCacheThumbnail= */ true); 81 } 82 LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info, Uri uri, boolean shouldCacheThumbnail)83 public LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info, Uri uri, 84 boolean shouldCacheThumbnail) { 85 mContext = context.getApplicationContext(); 86 mInfo = info; 87 mUri = uri; 88 mShouldCacheThumbnail = shouldCacheThumbnail; 89 mLayerResolver = InjectorProvider.getInjector().getDrawableLayerResolver(); 90 } 91 92 @Override decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, BitmapReceiver receiver)93 public void decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, 94 BitmapReceiver receiver) { 95 sExecutorService.execute(() -> { 96 Drawable thumb = getThumbnailDrawable(); 97 98 // Live wallpaper components may or may not specify a thumbnail drawable. 99 if (thumb instanceof BitmapDrawable) { 100 BitmapDrawable drawableThumb = (BitmapDrawable) thumb; 101 int thumbHeight = drawableThumb.getIntrinsicHeight(); 102 int thumbWidth = drawableThumb.getIntrinsicWidth(); 103 int height, width; 104 if (thumbHeight > 0 && thumbWidth > 0) { 105 double ratio = thumbHeight > thumbWidth ? (double) targetHeight / thumbHeight 106 : (double) targetWidth / thumbWidth; 107 height = (int) (thumbHeight * ratio); 108 width = (int) (thumbWidth * ratio); 109 } else { 110 height = targetHeight; 111 width = targetWidth; 112 } 113 decodeBitmapCompleted(receiver, 114 Bitmap.createScaledBitmap(drawableThumb.getBitmap(), width, height, true)); 115 return; 116 } else if (thumb != null) { 117 Bitmap bitmap; 118 if (thumb.getIntrinsicWidth() > 0 && thumb.getIntrinsicHeight() > 0) { 119 bitmap = Bitmap.createBitmap(thumb.getIntrinsicWidth(), 120 thumb.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); 121 } else { 122 decodeBitmapCompleted(receiver, null); 123 return; 124 } 125 126 Canvas canvas = new Canvas(bitmap); 127 thumb.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 128 thumb.draw(canvas); 129 decodeBitmapCompleted(receiver, 130 Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true)); 131 return; 132 } 133 decodeBitmapCompleted(receiver, null); 134 }); 135 } 136 137 @Override decodeBitmap(BitmapReceiver receiver)138 public void decodeBitmap(BitmapReceiver receiver) { 139 sExecutorService.execute(() -> { 140 Drawable thumb = getThumbnailDrawable(); 141 Bitmap bitmap = null; 142 // Live wallpaper components may or may not specify a thumbnail drawable. 143 if (thumb instanceof BitmapDrawable) { 144 bitmap = ((BitmapDrawable) thumb).getBitmap(); 145 } else if (thumb != null) { 146 if (thumb.getIntrinsicWidth() > 0 && thumb.getIntrinsicHeight() > 0) { 147 bitmap = Bitmap.createBitmap(thumb.getIntrinsicWidth(), 148 thumb.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); 149 } 150 } 151 decodeBitmapCompleted(receiver, bitmap); 152 }); 153 } 154 155 @Override decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)156 public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, 157 boolean shouldAdjustForRtl, BitmapReceiver receiver) { 158 receiver.onBitmapDecoded(null); 159 } 160 161 @Override decodeRawDimensions(Activity unused, DimensionsReceiver receiver)162 public void decodeRawDimensions(Activity unused, DimensionsReceiver receiver) { 163 // TODO(b/277166654): Reuse the logic for all thumb asset decoding 164 sExecutorService.execute(() -> { 165 Bitmap result = null; 166 Drawable thumb = mInfo.loadThumbnail(mContext.getPackageManager()); 167 if (thumb instanceof BitmapDrawable) { 168 result = ((BitmapDrawable) thumb).getBitmap(); 169 } else if (thumb instanceof LayerDrawable) { 170 Drawable layer = mLayerResolver.resolveLayer((LayerDrawable) thumb); 171 if (layer instanceof BitmapDrawable) { 172 result = ((BitmapDrawable) layer).getBitmap(); 173 } 174 } 175 final Bitmap lr = result; 176 new Handler(Looper.getMainLooper()).post( 177 () -> 178 receiver.onDimensionsDecoded( 179 lr == null ? null : new Point(lr.getWidth(), lr.getHeight())) 180 ); 181 }); 182 } 183 184 @Override supportsTiling()185 public boolean supportsTiling() { 186 return false; 187 } 188 189 @Override loadDrawable(Context context, ImageView imageView, int placeholderColor)190 public void loadDrawable(Context context, ImageView imageView, 191 int placeholderColor) { 192 RequestOptions reqOptions; 193 if (mUri != null) { 194 reqOptions = RequestOptions.centerCropTransform().apply(RequestOptions 195 .diskCacheStrategyOf(DiskCacheStrategy.NONE) 196 .skipMemoryCache(true)) 197 .placeholder(new ColorDrawable(placeholderColor)); 198 } else { 199 reqOptions = RequestOptions.centerCropTransform() 200 .placeholder(new ColorDrawable(placeholderColor)); 201 } 202 imageView.setBackgroundColor(placeholderColor); 203 Glide.with(context) 204 .asDrawable() 205 .load(LiveWallpaperThumbAsset.this) 206 .apply(reqOptions) 207 .transition(DrawableTransitionOptions.withCrossFade()) 208 .into(imageView); 209 } 210 211 @Override loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)212 public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, 213 BitmapTransformation transformation) { 214 Transformation<Bitmap> finalTransformation = (transformation == null) 215 ? new FitCenter() 216 : new MultiTransformation<>(new FitCenter(), transformation); 217 Glide.with(activity) 218 .asDrawable() 219 .load(LiveWallpaperThumbAsset.this) 220 .apply(RequestOptions.bitmapTransform(finalTransformation) 221 .placeholder(new ColorDrawable(placeholderColor))) 222 .into(imageView); 223 } 224 225 @Override 226 @WorkerThread getLowResBitmap(Context context)227 public Bitmap getLowResBitmap(Context context) { 228 try { 229 Drawable drawable = Glide.with(context) 230 .asDrawable() 231 .load(this) 232 .submit() 233 .get(LOW_RES_THUMB_TIMEOUT_SECONDS, TimeUnit.SECONDS); 234 235 if (drawable instanceof BitmapDrawable) { 236 BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; 237 Bitmap bitmap = bitmapDrawable.getBitmap(); 238 if (bitmap != null) { 239 return bitmap; 240 } 241 } 242 Bitmap bitmap; 243 // If not a bitmap, draw the drawable into a bitmap 244 if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { 245 return null; 246 } else { 247 bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), 248 drawable.getIntrinsicHeight(), Bitmap.Config.RGB_565); 249 } 250 251 Canvas canvas = new Canvas(bitmap); 252 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 253 drawable.draw(canvas); 254 return bitmap; 255 } catch (InterruptedException | ExecutionException | TimeoutException e) { 256 Log.w(TAG, "Couldn't obtain low res bitmap", e); 257 } 258 return null; 259 } 260 261 /** 262 * Returns a Glide cache key. 263 */ getKey()264 Key getKey() { 265 return new LiveWallpaperThumbKey(mInfo); 266 } 267 268 /** 269 * Returns the thumbnail drawable for the live wallpaper synchronously. Should not be called on 270 * the main UI thread. 271 * 272 * <p>Cache the thumbnail if {@code mShouldCacheThumbnail} is true. 273 */ 274 @WorkerThread getThumbnailDrawable()275 protected Drawable getThumbnailDrawable() { 276 if (!mShouldCacheThumbnail) { 277 return loadThumbnailFromUri(); 278 } 279 280 if (mCachedThumbnail != null) { 281 return mCachedThumbnail; 282 } 283 284 mCachedThumbnail = loadThumbnailFromUri(); 285 if (mCachedThumbnail == null) { 286 mCachedThumbnail = loadThumbnailFromInfo(); 287 } 288 289 return mCachedThumbnail; 290 } 291 loadThumbnailFromUri()292 private Drawable loadThumbnailFromUri() { 293 if (mUri != null) { 294 try (AssetFileDescriptor assetFileDescriptor = 295 mContext.getContentResolver().openAssetFileDescriptor(mUri, "r")) { 296 if (assetFileDescriptor != null) { 297 return new BitmapDrawable(mContext.getResources(), 298 BitmapFactory.decodeStream(assetFileDescriptor.createInputStream())); 299 } 300 } catch (IOException e) { 301 Log.w(TAG, "Not found thumbnail from URI."); 302 } 303 } 304 return null; 305 } 306 loadThumbnailFromInfo()307 private Drawable loadThumbnailFromInfo() { 308 return mInfo.loadThumbnail(mContext.getPackageManager()); 309 } 310 311 /** 312 * Glide caching key for resources from any arbitrary package. 313 */ 314 private static final class LiveWallpaperThumbKey implements Key { 315 private android.app.WallpaperInfo mInfo; 316 LiveWallpaperThumbKey(android.app.WallpaperInfo info)317 public LiveWallpaperThumbKey(android.app.WallpaperInfo info) { 318 mInfo = info; 319 } 320 321 @Override toString()322 public String toString() { 323 return getCacheKey(); 324 } 325 326 @Override hashCode()327 public int hashCode() { 328 return getCacheKey().hashCode(); 329 } 330 331 @Override equals(Object object)332 public boolean equals(Object object) { 333 if (!(object instanceof LiveWallpaperThumbKey)) { 334 return false; 335 } 336 337 LiveWallpaperThumbKey otherKey = (LiveWallpaperThumbKey) object; 338 return getCacheKey().equals(otherKey.getCacheKey()); 339 } 340 341 @Override updateDiskCacheKey(MessageDigest messageDigest)342 public void updateDiskCacheKey(MessageDigest messageDigest) { 343 messageDigest.update(getCacheKey().getBytes(CHARSET)); 344 } 345 346 /** 347 * Returns an inexpensively calculated {@link String} suitable for use as a disk cache key, 348 * based on the live wallpaper's package name and service name, which is enough to uniquely 349 * identify a live wallpaper. 350 */ getCacheKey()351 private String getCacheKey() { 352 return "LiveWallpaperThumbKey{" 353 + "packageName=" + mInfo.getPackageName() + "," 354 + "serviceName=" + mInfo.getServiceName() 355 + '}'; 356 } 357 } 358 } 359