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.pm.PackageManager;
21 import android.content.res.AssetFileDescriptor;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.Canvas;
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.net.Uri;
30 import android.os.AsyncTask;
31 import android.util.Log;
32 import android.widget.ImageView;
33 
34 import androidx.annotation.WorkerThread;
35 
36 import com.bumptech.glide.Glide;
37 import com.bumptech.glide.load.Key;
38 import com.bumptech.glide.load.MultiTransformation;
39 import com.bumptech.glide.load.Transformation;
40 import com.bumptech.glide.load.engine.DiskCacheStrategy;
41 import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
42 import com.bumptech.glide.load.resource.bitmap.FitCenter;
43 import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
44 import com.bumptech.glide.request.RequestOptions;
45 
46 import java.io.IOException;
47 import java.security.MessageDigest;
48 import java.util.concurrent.ExecutionException;
49 import java.util.concurrent.TimeUnit;
50 import java.util.concurrent.TimeoutException;
51 
52 /**
53  * Asset wrapping a drawable for a live wallpaper thumbnail.
54  */
55 public class LiveWallpaperThumbAsset extends Asset {
56     private static final String TAG = "LiveWallpaperThumbAsset";
57     private static final int LOW_RES_THUMB_TIMEOUT_SECONDS = 2;
58 
59     protected final Context mContext;
60     protected final android.app.WallpaperInfo mInfo;
61     // The content Uri of thumbnail
62     protected Uri mUri;
63     private Drawable mThumbnailDrawable;
64 
LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info)65     public LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info) {
66         mContext = context.getApplicationContext();
67         mInfo = info;
68     }
69 
LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info, Uri uri)70     public LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info, Uri uri) {
71         this(context, info);
72         mUri = uri;
73     }
74 
75     @Override
decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver)76     public void decodeBitmap(int targetWidth, int targetHeight,
77                              BitmapReceiver receiver) {
78         // No scaling is needed, as the thumbnail is already a thumbnail.
79         LoadThumbnailTask task = new LoadThumbnailTask(mContext, mInfo, receiver);
80         task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
81     }
82 
83     @Override
decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)84     public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight,
85             boolean shouldAdjustForRtl, BitmapReceiver receiver) {
86         receiver.onBitmapDecoded(null);
87     }
88 
89     @Override
decodeRawDimensions(Activity unused, DimensionsReceiver receiver)90     public void decodeRawDimensions(Activity unused, DimensionsReceiver receiver) {
91         receiver.onDimensionsDecoded(null);
92     }
93 
94     @Override
supportsTiling()95     public boolean supportsTiling() {
96         return false;
97     }
98 
99     @Override
loadDrawable(Context context, ImageView imageView, int placeholderColor)100     public void loadDrawable(Context context, ImageView imageView,
101                              int placeholderColor) {
102         RequestOptions reqOptions;
103         if (mUri != null) {
104             reqOptions = RequestOptions.centerCropTransform().apply(RequestOptions
105                     .diskCacheStrategyOf(DiskCacheStrategy.NONE)
106                     .skipMemoryCache(true))
107                     .placeholder(new ColorDrawable(placeholderColor));
108         } else {
109             reqOptions = RequestOptions.centerCropTransform()
110                     .placeholder(new ColorDrawable(placeholderColor));
111         }
112         Glide.with(context)
113                 .asDrawable()
114                 .load(LiveWallpaperThumbAsset.this)
115                 .apply(reqOptions)
116                 .transition(DrawableTransitionOptions.withCrossFade())
117                 .into(imageView);
118     }
119 
120     @Override
loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)121     public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor,
122             BitmapTransformation transformation) {
123         Transformation<Bitmap> finalTransformation = (transformation == null)
124                 ? new FitCenter()
125                 : new MultiTransformation<>(new FitCenter(), transformation);
126         Glide.with(activity)
127                 .asDrawable()
128                 .load(LiveWallpaperThumbAsset.this)
129                 .apply(RequestOptions.bitmapTransform(finalTransformation)
130                         .placeholder(new ColorDrawable(placeholderColor)))
131                 .into(imageView);
132     }
133 
134     @Override
135     @WorkerThread
getLowResBitmap(Context context)136     public Bitmap getLowResBitmap(Context context) {
137         try {
138             Drawable drawable = Glide.with(context)
139                     .asDrawable()
140                     .load(this)
141                     .submit()
142                     .get(LOW_RES_THUMB_TIMEOUT_SECONDS, TimeUnit.SECONDS);
143 
144             if (drawable instanceof BitmapDrawable) {
145                 BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
146                 Bitmap bitmap = bitmapDrawable.getBitmap();
147                 if (bitmap != null) {
148                     return bitmap;
149                 }
150             }
151             Bitmap bitmap;
152             // If not a bitmap, draw the drawable into a bitmap
153             if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
154                 return null;
155             } else {
156                 bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
157                         drawable.getIntrinsicHeight(), Bitmap.Config.RGB_565);
158             }
159 
160             Canvas canvas = new Canvas(bitmap);
161             drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
162             drawable.draw(canvas);
163             return bitmap;
164         } catch (InterruptedException | ExecutionException | TimeoutException e) {
165             Log.w(TAG, "Couldn't obtain low res bitmap", e);
166         }
167         return null;
168     }
169 
170     /**
171      * Returns a Glide cache key.
172      */
getKey()173     Key getKey() {
174         return new LiveWallpaperThumbKey(mInfo);
175     }
176 
177     /**
178      * Returns the thumbnail drawable for the live wallpaper synchronously. Should not be called on
179      * the main UI thread.
180      */
getThumbnailDrawable()181     protected Drawable getThumbnailDrawable() {
182         if (mThumbnailDrawable != null) {
183             return mThumbnailDrawable;
184         }
185         if (mUri != null) {
186             try (AssetFileDescriptor assetFileDescriptor =
187                          mContext.getContentResolver().openAssetFileDescriptor(mUri, "r")) {
188                 if (assetFileDescriptor != null) {
189                     mThumbnailDrawable = new BitmapDrawable(mContext.getResources(),
190                             BitmapFactory.decodeStream(assetFileDescriptor.createInputStream()));
191                     return mThumbnailDrawable;
192                 }
193             } catch (IOException e) {
194                 Log.w(TAG, "Not found thumbnail from URI.");
195             }
196         }
197         mThumbnailDrawable = mInfo.loadThumbnail(mContext.getPackageManager());
198         return mThumbnailDrawable;
199     }
200 
201     /**
202      * Glide caching key for resources from any arbitrary package.
203      */
204     private static final class LiveWallpaperThumbKey implements Key {
205         private android.app.WallpaperInfo mInfo;
206 
LiveWallpaperThumbKey(android.app.WallpaperInfo info)207         public LiveWallpaperThumbKey(android.app.WallpaperInfo info) {
208             mInfo = info;
209         }
210 
211         @Override
toString()212         public String toString() {
213             return getCacheKey();
214         }
215 
216         @Override
hashCode()217         public int hashCode() {
218             return getCacheKey().hashCode();
219         }
220 
221         @Override
equals(Object object)222         public boolean equals(Object object) {
223             if (!(object instanceof LiveWallpaperThumbKey)) {
224                 return false;
225             }
226 
227             LiveWallpaperThumbKey otherKey = (LiveWallpaperThumbKey) object;
228             return getCacheKey().equals(otherKey.getCacheKey());
229         }
230 
231         @Override
updateDiskCacheKey(MessageDigest messageDigest)232         public void updateDiskCacheKey(MessageDigest messageDigest) {
233             messageDigest.update(getCacheKey().getBytes(CHARSET));
234         }
235 
236         /**
237          * Returns an inexpensively calculated {@link String} suitable for use as a disk cache key,
238          * based on the live wallpaper's package name and service name, which is enough to uniquely
239          * identify a live wallpaper.
240          */
getCacheKey()241         private String getCacheKey() {
242             return "LiveWallpaperThumbKey{"
243                     + "packageName=" + mInfo.getPackageName() + ","
244                     + "serviceName=" + mInfo.getServiceName()
245                     + '}';
246         }
247     }
248 
249     /**
250      * AsyncTask subclass which loads the live wallpaper's thumbnail bitmap off the main UI thread.
251      * Resolves with null if live wallpaper thumbnail is not a bitmap.
252      */
253     private static class LoadThumbnailTask extends AsyncTask<Void, Void, Bitmap> {
254         private final PackageManager mPackageManager;
255         private android.app.WallpaperInfo mInfo;
256         private BitmapReceiver mReceiver;
257 
LoadThumbnailTask(Context context, android.app.WallpaperInfo info, BitmapReceiver receiver)258         public LoadThumbnailTask(Context context, android.app.WallpaperInfo info,
259                 BitmapReceiver receiver) {
260             mInfo = info;
261             mReceiver = receiver;
262             mPackageManager = context.getPackageManager();
263         }
264 
265         @Override
doInBackground(Void... unused)266         protected Bitmap doInBackground(Void... unused) {
267             Drawable thumb = mInfo.loadThumbnail(mPackageManager);
268 
269             // Live wallpaper components may or may not specify a thumbnail drawable.
270             if (thumb instanceof BitmapDrawable) {
271                 return ((BitmapDrawable) thumb).getBitmap();
272             } else if (thumb != null) {
273                 Bitmap bitmap;
274                 if (thumb.getIntrinsicWidth() > 0 && thumb.getIntrinsicHeight() > 0) {
275                     bitmap = Bitmap.createBitmap(thumb.getIntrinsicWidth(),
276                             thumb.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
277                 } else {
278                     return null;
279                 }
280 
281                 Canvas canvas = new Canvas(bitmap);
282                 thumb.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
283                 thumb.draw(canvas);
284                 return bitmap;
285             }
286 
287             // If no thumbnail was specified, return a null bitmap.
288             return null;
289         }
290 
291         @Override
onPostExecute(Bitmap bitmap)292         protected void onPostExecute(Bitmap bitmap) {
293             mReceiver.onBitmapDecoded(bitmap);
294         }
295     }
296 }
297