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