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