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