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.Resources;
21 import android.graphics.Bitmap;
22 import android.graphics.Bitmap.Config;
23 import android.graphics.Point;
24 import android.graphics.Rect;
25 import android.graphics.drawable.BitmapDrawable;
26 import android.graphics.drawable.ColorDrawable;
27 import android.graphics.drawable.Drawable;
28 import android.graphics.drawable.TransitionDrawable;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.view.Display;
32 import android.view.View;
33 import android.widget.ImageView;
34 
35 import androidx.annotation.Nullable;
36 import androidx.annotation.WorkerThread;
37 
38 import com.android.wallpaper.module.BitmapCropper;
39 import com.android.wallpaper.module.InjectorProvider;
40 import com.android.wallpaper.picker.preview.ui.util.CropSizeUtil;
41 import com.android.wallpaper.util.RtlUtils;
42 import com.android.wallpaper.util.ScreenSizeCalculator;
43 import com.android.wallpaper.util.WallpaperCropUtils;
44 
45 import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
46 
47 import java.io.File;
48 import java.util.Map;
49 import java.util.concurrent.ExecutorService;
50 import java.util.concurrent.Executors;
51 
52 /**
53  * Interface representing an image asset.
54  */
55 public abstract class Asset {
56     private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor();
57     /**
58      * Creates and returns a placeholder Drawable instance sized exactly to the target ImageView and
59      * filled completely with pixels of the provided placeholder color.
60      */
getPlaceholderDrawable( Context context, ImageView imageView, int placeholderColor)61     protected static Drawable getPlaceholderDrawable(
62             Context context, ImageView imageView, int placeholderColor) {
63         Point imageViewDimensions = getViewDimensions(imageView);
64         Bitmap placeholderBitmap =
65                 Bitmap.createBitmap(imageViewDimensions.x, imageViewDimensions.y, Config.ARGB_8888);
66         placeholderBitmap.eraseColor(placeholderColor);
67         return new BitmapDrawable(context.getResources(), placeholderBitmap);
68     }
69 
70     /**
71      * Returns the visible height and width in pixels of the provided ImageView, or if it hasn't
72      * been laid out yet, then gets the absolute value of the layout params.
73      */
getViewDimensions(View view)74     private static Point getViewDimensions(View view) {
75         int width = view.getWidth() > 0 ? view.getWidth() : Math.abs(view.getLayoutParams().width);
76         int height = view.getHeight() > 0 ? view.getHeight()
77                 : Math.abs(view.getLayoutParams().height);
78 
79         return new Point(width, height);
80     }
81 
82     /**
83      * Decodes a bitmap sized for the destination view's dimensions off the main UI thread.
84      *
85      * @param targetWidth  Width of target view in physical pixels.
86      * @param targetHeight Height of target view in physical pixels.
87      * @param receiver     Called with the decoded bitmap or null if there was an error decoding the
88      *                     bitmap.
89      */
decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver)90     public final void decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver) {
91         decodeBitmap(targetWidth, targetHeight, true, receiver);
92     }
93 
94 
95     /**
96      * Decodes a bitmap sized for the destination view's dimensions off the main UI thread.
97      *
98      * @param targetWidth  Width of target view in physical pixels.
99      * @param targetHeight Height of target view in physical pixels.
100      * @param hardwareBitmapAllowed if true and it's possible, we'll try to decode into a HARDWARE
101      *                              bitmap
102      * @param receiver     Called with the decoded bitmap or null if there was an error decoding the
103      *                     bitmap.
104      */
decodeBitmap(int targetWidth, int targetHeight, boolean hardwareBitmapAllowed, BitmapReceiver receiver)105     public abstract void decodeBitmap(int targetWidth, int targetHeight,
106             boolean hardwareBitmapAllowed, BitmapReceiver receiver);
107 
108     /**
109      * Copies the asset file to another place.
110      * @param dest  The destination file.
111      */
copy(File dest)112     public void copy(File dest) {
113         // no op
114     }
115 
116     /**
117      * Decodes a full bitmap.
118      *
119      * @param receiver     Called with the decoded bitmap or null if there was an error decoding the
120      *                     bitmap.
121      */
decodeBitmap(BitmapReceiver receiver)122     public abstract void decodeBitmap(BitmapReceiver receiver);
123 
124     /**
125      * For {@link #decodeBitmap(int, int, BitmapReceiver)} to use when it is done. It then call
126      * the receiver with decoded bitmap in the main thread.
127      *
128      * @param receiver The receiver to handle decoded bitmap or null if decoding failed.
129      * @param decodedBitmap The bitmap which is already decoded.
130      */
decodeBitmapCompleted(BitmapReceiver receiver, Bitmap decodedBitmap)131     protected void decodeBitmapCompleted(BitmapReceiver receiver, Bitmap decodedBitmap) {
132         new Handler(Looper.getMainLooper()).post(() -> receiver.onBitmapDecoded(decodedBitmap));
133     }
134 
135     /**
136      * Decodes and downscales a bitmap region off the main UI thread.
137      * @param rect         Rect representing the crop region in terms of the original image's
138      *                     resolution.
139      * @param targetWidth  Width of target view in physical pixels.
140      * @param targetHeight Height of target view in physical pixels.
141      * @param shouldAdjustForRtl whether the region selected should be adjusted for RTL (that is,
142      *                           the crop region will be considered starting from the right)
143      * @param receiver     Called with the decoded bitmap region or null if there was an error
144      */
decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)145     public abstract void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight,
146             boolean shouldAdjustForRtl, BitmapReceiver receiver);
147 
148     /**
149      * Calculates the raw dimensions of the asset at its original resolution off the main UI thread.
150      * Avoids decoding the entire bitmap if possible to conserve memory.
151      *
152      * @param activity Activity in which this decoding request is made. Allows for early termination
153      *                 of fetching image data and/or decoding to a bitmap. May be null, in which
154      *                 case the request is made in the application context instead.
155      * @param receiver Called with the decoded raw dimensions of the whole image or null if there
156      *                 was an error decoding the dimensions.
157      */
decodeRawDimensions(@ullable Activity activity, DimensionsReceiver receiver)158     public abstract void decodeRawDimensions(@Nullable Activity activity,
159             DimensionsReceiver receiver);
160 
161     /**
162      * Returns whether this asset has access to a separate, lower fidelity source of image data
163      * (that may be able to be loaded more quickly to simulate progressive loading).
164      */
hasLowResDataSource()165     public boolean hasLowResDataSource() {
166         return false;
167     }
168 
169     /**
170      * Loads the asset from the separate low resolution data source (if there is one) into the
171      * provided ImageView with the placeholder color and bitmap transformation.
172      *
173      * @param transformation Bitmap transformation that can transform the thumbnail image
174      *                       post-decoding.
175      */
loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)176     public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor,
177             BitmapTransformation transformation) {
178         // No op
179     }
180 
181     /**
182      * Returns a Bitmap from the separate low resolution data source (if there is one) or
183      * {@code null} otherwise.
184      * This could be an I/O operation so DO NOT CALL ON UI THREAD
185      */
186     @WorkerThread
187     @Nullable
getLowResBitmap(Context context)188     public Bitmap getLowResBitmap(Context context) {
189         return null;
190     }
191 
192     /**
193      * Returns whether the asset supports rendering tile regions at varying pixel densities.
194      */
supportsTiling()195     public abstract boolean supportsTiling();
196 
197     /**
198      * Loads a Drawable for this asset into the provided ImageView. While waiting for the image to
199      * load, first loads a ColorDrawable based on the provided placeholder color.
200      *
201      * @param context          Activity hosting the ImageView.
202      * @param imageView        ImageView which is the target view of this asset.
203      * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
204      *                         load.
205      */
loadDrawable(final Context context, final ImageView imageView, int placeholderColor)206     public void loadDrawable(final Context context, final ImageView imageView,
207             int placeholderColor) {
208         // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
209         // question is empty.
210         final boolean needsTransition = imageView.getDrawable() == null;
211         final Drawable placeholderDrawable = new ColorDrawable(placeholderColor);
212         if (needsTransition) {
213             imageView.setImageDrawable(placeholderDrawable);
214         }
215 
216         // Set requested height and width to the either the actual height and width of the view in
217         // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout
218         // params.
219         int width = imageView.getWidth() > 0
220                 ? imageView.getWidth()
221                 : Math.abs(imageView.getLayoutParams().width);
222         int height = imageView.getHeight() > 0
223                 ? imageView.getHeight()
224                 : Math.abs(imageView.getLayoutParams().height);
225 
226         decodeBitmap(width, height, new BitmapReceiver() {
227             @Override
228             public void onBitmapDecoded(Bitmap bitmap) {
229                 if (!needsTransition) {
230                     imageView.setImageBitmap(bitmap);
231                     return;
232                 }
233 
234                 Resources resources = context.getResources();
235 
236                 Drawable[] layers = new Drawable[2];
237                 layers[0] = placeholderDrawable;
238                 layers[1] = new BitmapDrawable(resources, bitmap);
239 
240                 TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
241                 transitionDrawable.setCrossFadeEnabled(true);
242 
243                 imageView.setImageDrawable(transitionDrawable);
244                 transitionDrawable.startTransition(resources.getInteger(
245                         android.R.integer.config_shortAnimTime));
246             }
247         });
248     }
249 
250     /**
251      * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition
252      * with the given duration from the Drawable previously set on the ImageView.
253      *
254      * @param context                  Activity hosting the ImageView.
255      * @param imageView                ImageView which is the target view of this asset.
256      * @param transitionDurationMillis Duration of the crossfade, in milliseconds.
257      * @param drawableLoadedListener   Listener called once the transition has begun.
258      * @param placeholderColor         Color of the placeholder if the provided ImageView is empty
259      *                                 before the
260      */
loadDrawableWithTransition( final Context context, final ImageView imageView, final int transitionDurationMillis, @Nullable final DrawableLoadedListener drawableLoadedListener, int placeholderColor)261     public void loadDrawableWithTransition(
262             final Context context,
263             final ImageView imageView,
264             final int transitionDurationMillis,
265             @Nullable final DrawableLoadedListener drawableLoadedListener,
266             int placeholderColor) {
267         Point imageViewDimensions = getViewDimensions(imageView);
268 
269         // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
270         // question is empty.
271         boolean needsPlaceholder = imageView.getDrawable() == null;
272         if (needsPlaceholder) {
273             imageView.setImageDrawable(
274                     getPlaceholderDrawable(context, imageView, placeholderColor));
275         }
276 
277         decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() {
278             @Override
279             public void onBitmapDecoded(Bitmap bitmap) {
280                 final Resources resources = context.getResources();
281 
282                 centerCropBitmap(bitmap, imageView, new BitmapReceiver() {
283                     @Override
284                     public void onBitmapDecoded(@Nullable Bitmap newBitmap) {
285                         Drawable[] layers = new Drawable[2];
286                         Drawable existingDrawable = imageView.getDrawable();
287 
288                         if (existingDrawable instanceof TransitionDrawable) {
289                             // Take only the second layer in the existing TransitionDrawable so
290                             // we don't keep
291                             // around a reference to older layers which are no longer shown (this
292                             // way we avoid a
293                             // memory leak).
294                             TransitionDrawable existingTransitionDrawable =
295                                     (TransitionDrawable) existingDrawable;
296                             int id = existingTransitionDrawable.getId(1);
297                             layers[0] = existingTransitionDrawable.findDrawableByLayerId(id);
298                         } else {
299                             layers[0] = existingDrawable;
300                         }
301                         layers[1] = new BitmapDrawable(resources, newBitmap);
302 
303                         TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
304                         transitionDrawable.setCrossFadeEnabled(true);
305 
306                         imageView.setImageDrawable(transitionDrawable);
307                         transitionDrawable.startTransition(transitionDurationMillis);
308 
309                         if (drawableLoadedListener != null) {
310                             drawableLoadedListener.onDrawableLoaded();
311                         }
312                     }
313                 });
314             }
315         });
316     }
317 
318     /**
319      * Loads the image for this asset into the provided ImageView which is used for the preview.
320      * While waiting for the image to load, first loads a ColorDrawable based on the provided
321      * placeholder color.
322      *
323      * @param activity         Activity hosting the ImageView.
324      * @param imageView        ImageView which is the target view of this asset.
325      * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
326      *                         load.
327      * @param offsetToStart    true to let the preview show from the start of the image, false to
328      *                         center-aligned to the image.
329      */
loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, boolean offsetToStart)330     public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor,
331             boolean offsetToStart) {
332         loadPreviewImage(activity, imageView, placeholderColor, offsetToStart, null);
333     }
334 
335     /**
336      * Loads the image for this asset into the provided ImageView which is used for the preview.
337      * While waiting for the image to load, first loads a ColorDrawable based on the provided
338      * placeholder color.
339      *
340      * @param activity         Activity hosting the ImageView.
341      * @param imageView        ImageView which is the target view of this asset.
342      * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
343      *                         load.
344      * @param offsetToStart    true to let the preview show from the start of the image, false to
345      *                         center-aligned to the image.
346      * @param cropHints        A Map of display size to crop rect
347      */
loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, boolean offsetToStart, @Nullable Map<Point, Rect> cropHints)348     public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor,
349             boolean offsetToStart, @Nullable Map<Point, Rect> cropHints) {
350         boolean needsTransition = imageView.getDrawable() == null;
351         Drawable placeholderDrawable = new ColorDrawable(placeholderColor);
352         if (needsTransition) {
353             imageView.setImageDrawable(placeholderDrawable);
354         }
355 
356         decodeRawDimensions(activity, dimensions -> {
357             // TODO (b/286404249): A proper fix here would be to find out why the
358             //  leak happens in first place
359             if (activity.isDestroyed()) {
360                 return;
361             }
362             if (dimensions == null) {
363                 loadDrawable(activity, imageView, placeholderColor);
364                 return;
365             }
366 
367             boolean isRtl = RtlUtils.isRtl(activity);
368             Display defaultDisplay = activity.getWindowManager().getDefaultDisplay();
369             Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay);
370             Rect visibleRawWallpaperRect =
371                     WallpaperCropUtils.calculateVisibleRect(dimensions, screenSize);
372             if (cropHints != null && cropHints.containsKey(screenSize)) {
373                 visibleRawWallpaperRect = CropSizeUtil.INSTANCE.fitCropRectToLayoutDirection(
374                         cropHints.get(screenSize), screenSize, RtlUtils.isRtl(activity));
375                 // For multi-crop, the visibleRawWallpaperRect above is already the exact size of
376                 // the part of wallpaper we should show on the screen, turning off the old RTL
377                 // logic by assigning false.
378                 isRtl = false;
379             }
380 
381             // TODO(b/264234793): Make offsetToStart general support or for the specific asset.
382             adjustCropRect(activity, dimensions, visibleRawWallpaperRect, offsetToStart);
383 
384             BitmapCropper bitmapCropper = InjectorProvider.getInjector().getBitmapCropper();
385             bitmapCropper.cropAndScaleBitmap(this, /* scale= */ 1f, visibleRawWallpaperRect,
386                     isRtl,
387                     new BitmapCropper.Callback() {
388                         @Override
389                         public void onBitmapCropped(Bitmap croppedBitmap) {
390                             // Since the size of the cropped bitmap may not exactly the same with
391                             // image view(maybe has 1px or 2px difference),
392                             // so set CENTER_CROP to let the bitmap to fit the image view.
393                             if (!activity.isDestroyed()) {
394                                 imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
395                                 if (!needsTransition) {
396                                     imageView.setImageBitmap(croppedBitmap);
397                                     return;
398                                 }
399 
400                                 Resources resources = activity.getResources();
401 
402                                 Drawable[] layers = new Drawable[2];
403                                 layers[0] = placeholderDrawable;
404                                 layers[1] = new BitmapDrawable(resources, croppedBitmap);
405 
406                                 TransitionDrawable transitionDrawable = new
407                                         TransitionDrawable(layers);
408                                 transitionDrawable.setCrossFadeEnabled(true);
409 
410                                 imageView.setImageDrawable(transitionDrawable);
411                                 transitionDrawable.startTransition(resources.getInteger(
412                                         android.R.integer.config_shortAnimTime));
413                             }
414                         }
415 
416                         @Override
417                         public void onError(@Nullable Throwable e) {
418                             if (!activity.isDestroyed()) {
419                                 loadDrawable(activity, imageView, placeholderColor);
420                             }
421                         }
422                     });
423         });
424     }
425 
426     /**
427      * Interface for receiving decoded Bitmaps.
428      */
429     public interface BitmapReceiver {
430 
431         /**
432          * Called with a decoded Bitmap object or null if there was an error decoding the bitmap.
433          */
onBitmapDecoded(@ullable Bitmap bitmap)434         void onBitmapDecoded(@Nullable Bitmap bitmap);
435     }
436 
437     /**
438      * Interface for receiving raw asset dimensions.
439      */
440     public interface DimensionsReceiver {
441 
442         /**
443          * Called with raw dimensions of asset or null if the asset is unable to decode the raw
444          * dimensions.
445          *
446          * @param dimensions Dimensions as a Point where width is represented by "x" and height by
447          *                   "y".
448          */
onDimensionsDecoded(@ullable Point dimensions)449         void onDimensionsDecoded(@Nullable Point dimensions);
450     }
451 
452     /**
453      * Interface for being notified when a drawable has been loaded.
454      */
455     public interface DrawableLoadedListener {
onDrawableLoaded()456         void onDrawableLoaded();
457     }
458 
adjustCropRect(Context context, Point assetDimensions, Rect cropRect, boolean offsetToStart)459     protected void adjustCropRect(Context context, Point assetDimensions, Rect cropRect,
460             boolean offsetToStart) {
461         WallpaperCropUtils.adjustCropRect(context, cropRect, true /* zoomIn */);
462     }
463 
464     /**
465      * Returns a copy of the given bitmap which is center cropped and scaled
466      * to fit in the given ImageView and the thread runs on ExecutorService.
467      */
centerCropBitmap(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver)468     public void centerCropBitmap(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver) {
469         Point imageViewDimensions = getViewDimensions(view);
470         sExecutorService.execute(() -> {
471             int measuredWidth = imageViewDimensions.x;
472             int measuredHeight = imageViewDimensions.y;
473 
474             int bitmapWidth = bitmap.getWidth();
475             int bitmapHeight = bitmap.getHeight();
476 
477             float scale = Math.min(
478                     (float) bitmapWidth / measuredWidth,
479                     (float) bitmapHeight / measuredHeight);
480 
481             Bitmap scaledBitmap = Bitmap.createScaledBitmap(
482                     bitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale),
483                     true);
484 
485             int horizontalGutterPx = Math.max(0, (scaledBitmap.getWidth() - measuredWidth) / 2);
486             int verticalGutterPx = Math.max(0, (scaledBitmap.getHeight() - measuredHeight) / 2);
487             Bitmap result = Bitmap.createBitmap(
488                     scaledBitmap,
489                     horizontalGutterPx,
490                     verticalGutterPx,
491                     scaledBitmap.getWidth() - (2 * horizontalGutterPx),
492                     scaledBitmap.getHeight() - (2 * verticalGutterPx));
493             decodeBitmapCompleted(bitmapReceiver, result);
494         });
495     }
496 }
497