|  | /* | 
|  | * Copyright (C) 2017 The Android Open Source Project | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. | 
|  | */ | 
|  | package com.android.wallpaper.asset; | 
|  |  | 
|  | import android.app.Activity; | 
|  | import android.content.Context; | 
|  | import android.content.res.Resources; | 
|  | import android.graphics.Bitmap; | 
|  | import android.graphics.Bitmap.Config; | 
|  | import android.graphics.Point; | 
|  | import android.graphics.Rect; | 
|  | import android.graphics.drawable.BitmapDrawable; | 
|  | import android.graphics.drawable.ColorDrawable; | 
|  | import android.graphics.drawable.Drawable; | 
|  | import android.graphics.drawable.TransitionDrawable; | 
|  | import android.os.Handler; | 
|  | import android.os.Looper; | 
|  | import android.view.Display; | 
|  | import android.view.View; | 
|  | import android.widget.ImageView; | 
|  |  | 
|  | import androidx.annotation.Nullable; | 
|  | import androidx.annotation.WorkerThread; | 
|  |  | 
|  | import com.android.wallpaper.module.BitmapCropper; | 
|  | import com.android.wallpaper.module.InjectorProvider; | 
|  | import com.android.wallpaper.picker.preview.ui.util.CropSizeUtil; | 
|  | import com.android.wallpaper.util.RtlUtils; | 
|  | import com.android.wallpaper.util.ScreenSizeCalculator; | 
|  | import com.android.wallpaper.util.WallpaperCropUtils; | 
|  |  | 
|  | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; | 
|  |  | 
|  | import java.io.File; | 
|  | import java.util.Map; | 
|  | import java.util.concurrent.ExecutorService; | 
|  | import java.util.concurrent.Executors; | 
|  |  | 
|  | /** | 
|  | * Interface representing an image asset. | 
|  | */ | 
|  | public abstract class Asset { | 
|  | private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor(); | 
|  | /** | 
|  | * Creates and returns a placeholder Drawable instance sized exactly to the target ImageView and | 
|  | * filled completely with pixels of the provided placeholder color. | 
|  | */ | 
|  | protected static Drawable getPlaceholderDrawable( | 
|  | Context context, ImageView imageView, int placeholderColor) { | 
|  | Point imageViewDimensions = getViewDimensions(imageView); | 
|  | Bitmap placeholderBitmap = | 
|  | Bitmap.createBitmap(imageViewDimensions.x, imageViewDimensions.y, Config.ARGB_8888); | 
|  | placeholderBitmap.eraseColor(placeholderColor); | 
|  | return new BitmapDrawable(context.getResources(), placeholderBitmap); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the visible height and width in pixels of the provided ImageView, or if it hasn't | 
|  | * been laid out yet, then gets the absolute value of the layout params. | 
|  | */ | 
|  | private static Point getViewDimensions(View view) { | 
|  | int width = view.getWidth() > 0 ? view.getWidth() : Math.abs(view.getLayoutParams().width); | 
|  | int height = view.getHeight() > 0 ? view.getHeight() | 
|  | : Math.abs(view.getLayoutParams().height); | 
|  |  | 
|  | return new Point(width, height); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Decodes a bitmap sized for the destination view's dimensions off the main UI thread. | 
|  | * | 
|  | * @param targetWidth  Width of target view in physical pixels. | 
|  | * @param targetHeight Height of target view in physical pixels. | 
|  | * @param receiver     Called with the decoded bitmap or null if there was an error decoding the | 
|  | *                     bitmap. | 
|  | */ | 
|  | public final void decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver) { | 
|  | decodeBitmap(targetWidth, targetHeight, true, receiver); | 
|  | } | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Decodes a bitmap sized for the destination view's dimensions off the main UI thread. | 
|  | * | 
|  | * @param targetWidth  Width of target view in physical pixels. | 
|  | * @param targetHeight Height of target view in physical pixels. | 
|  | * @param hardwareBitmapAllowed if true and it's possible, we'll try to decode into a HARDWARE | 
|  | *                              bitmap | 
|  | * @param receiver     Called with the decoded bitmap or null if there was an error decoding the | 
|  | *                     bitmap. | 
|  | */ | 
|  | public abstract void decodeBitmap(int targetWidth, int targetHeight, | 
|  | boolean hardwareBitmapAllowed, BitmapReceiver receiver); | 
|  |  | 
|  | /** | 
|  | * Copies the asset file to another place. | 
|  | * @param dest  The destination file. | 
|  | */ | 
|  | public void copy(File dest) { | 
|  | // no op | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Decodes a full bitmap. | 
|  | * | 
|  | * @param receiver     Called with the decoded bitmap or null if there was an error decoding the | 
|  | *                     bitmap. | 
|  | */ | 
|  | public abstract void decodeBitmap(BitmapReceiver receiver); | 
|  |  | 
|  | /** | 
|  | * For {@link #decodeBitmap(int, int, BitmapReceiver)} to use when it is done. It then call | 
|  | * the receiver with decoded bitmap in the main thread. | 
|  | * | 
|  | * @param receiver The receiver to handle decoded bitmap or null if decoding failed. | 
|  | * @param decodedBitmap The bitmap which is already decoded. | 
|  | */ | 
|  | protected void decodeBitmapCompleted(BitmapReceiver receiver, Bitmap decodedBitmap) { | 
|  | new Handler(Looper.getMainLooper()).post(() -> receiver.onBitmapDecoded(decodedBitmap)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Decodes and downscales a bitmap region off the main UI thread. | 
|  | * @param rect         Rect representing the crop region in terms of the original image's | 
|  | *                     resolution. | 
|  | * @param targetWidth  Width of target view in physical pixels. | 
|  | * @param targetHeight Height of target view in physical pixels. | 
|  | * @param shouldAdjustForRtl whether the region selected should be adjusted for RTL (that is, | 
|  | *                           the crop region will be considered starting from the right) | 
|  | * @param receiver     Called with the decoded bitmap region or null if there was an error | 
|  | */ | 
|  | public abstract void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, | 
|  | boolean shouldAdjustForRtl, BitmapReceiver receiver); | 
|  |  | 
|  | /** | 
|  | * Calculates the raw dimensions of the asset at its original resolution off the main UI thread. | 
|  | * Avoids decoding the entire bitmap if possible to conserve memory. | 
|  | * | 
|  | * @param activity Activity in which this decoding request is made. Allows for early termination | 
|  | *                 of fetching image data and/or decoding to a bitmap. May be null, in which | 
|  | *                 case the request is made in the application context instead. | 
|  | * @param receiver Called with the decoded raw dimensions of the whole image or null if there | 
|  | *                 was an error decoding the dimensions. | 
|  | */ | 
|  | public abstract void decodeRawDimensions(@Nullable Activity activity, | 
|  | DimensionsReceiver receiver); | 
|  |  | 
|  | /** | 
|  | * Returns whether this asset has access to a separate, lower fidelity source of image data | 
|  | * (that may be able to be loaded more quickly to simulate progressive loading). | 
|  | */ | 
|  | public boolean hasLowResDataSource() { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Loads the asset from the separate low resolution data source (if there is one) into the | 
|  | * provided ImageView with the placeholder color and bitmap transformation. | 
|  | * | 
|  | * @param transformation Bitmap transformation that can transform the thumbnail image | 
|  | *                       post-decoding. | 
|  | */ | 
|  | public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, | 
|  | BitmapTransformation transformation) { | 
|  | // No op | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a Bitmap from the separate low resolution data source (if there is one) or | 
|  | * {@code null} otherwise. | 
|  | * This could be an I/O operation so DO NOT CALL ON UI THREAD | 
|  | */ | 
|  | @WorkerThread | 
|  | @Nullable | 
|  | public Bitmap getLowResBitmap(Context context) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns whether the asset supports rendering tile regions at varying pixel densities. | 
|  | */ | 
|  | public abstract boolean supportsTiling(); | 
|  |  | 
|  | /** | 
|  | * Loads a Drawable for this asset into the provided ImageView. While waiting for the image to | 
|  | * load, first loads a ColorDrawable based on the provided placeholder color. | 
|  | * | 
|  | * @param context          Activity hosting the ImageView. | 
|  | * @param imageView        ImageView which is the target view of this asset. | 
|  | * @param placeholderColor Color of placeholder set to ImageView while waiting for image to | 
|  | *                         load. | 
|  | */ | 
|  | public void loadDrawable(final Context context, final ImageView imageView, | 
|  | int placeholderColor) { | 
|  | // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in | 
|  | // question is empty. | 
|  | final boolean needsTransition = imageView.getDrawable() == null; | 
|  | final Drawable placeholderDrawable = new ColorDrawable(placeholderColor); | 
|  | if (needsTransition) { | 
|  | imageView.setImageDrawable(placeholderDrawable); | 
|  | } | 
|  |  | 
|  | // Set requested height and width to the either the actual height and width of the view in | 
|  | // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout | 
|  | // params. | 
|  | int width = imageView.getWidth() > 0 | 
|  | ? imageView.getWidth() | 
|  | : Math.abs(imageView.getLayoutParams().width); | 
|  | int height = imageView.getHeight() > 0 | 
|  | ? imageView.getHeight() | 
|  | : Math.abs(imageView.getLayoutParams().height); | 
|  |  | 
|  | decodeBitmap(width, height, new BitmapReceiver() { | 
|  | @Override | 
|  | public void onBitmapDecoded(Bitmap bitmap) { | 
|  | if (!needsTransition) { | 
|  | imageView.setImageBitmap(bitmap); | 
|  | return; | 
|  | } | 
|  |  | 
|  | Resources resources = context.getResources(); | 
|  |  | 
|  | Drawable[] layers = new Drawable[2]; | 
|  | layers[0] = placeholderDrawable; | 
|  | layers[1] = new BitmapDrawable(resources, bitmap); | 
|  |  | 
|  | TransitionDrawable transitionDrawable = new TransitionDrawable(layers); | 
|  | transitionDrawable.setCrossFadeEnabled(true); | 
|  |  | 
|  | imageView.setImageDrawable(transitionDrawable); | 
|  | transitionDrawable.startTransition(resources.getInteger( | 
|  | android.R.integer.config_shortAnimTime)); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition | 
|  | * with the given duration from the Drawable previously set on the ImageView. | 
|  | * | 
|  | * @param context                  Activity hosting the ImageView. | 
|  | * @param imageView                ImageView which is the target view of this asset. | 
|  | * @param transitionDurationMillis Duration of the crossfade, in milliseconds. | 
|  | * @param drawableLoadedListener   Listener called once the transition has begun. | 
|  | * @param placeholderColor         Color of the placeholder if the provided ImageView is empty | 
|  | *                                 before the | 
|  | */ | 
|  | public void loadDrawableWithTransition( | 
|  | final Context context, | 
|  | final ImageView imageView, | 
|  | final int transitionDurationMillis, | 
|  | @Nullable final DrawableLoadedListener drawableLoadedListener, | 
|  | int placeholderColor) { | 
|  | Point imageViewDimensions = getViewDimensions(imageView); | 
|  |  | 
|  | // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in | 
|  | // question is empty. | 
|  | boolean needsPlaceholder = imageView.getDrawable() == null; | 
|  | if (needsPlaceholder) { | 
|  | imageView.setImageDrawable( | 
|  | getPlaceholderDrawable(context, imageView, placeholderColor)); | 
|  | } | 
|  |  | 
|  | decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() { | 
|  | @Override | 
|  | public void onBitmapDecoded(Bitmap bitmap) { | 
|  | final Resources resources = context.getResources(); | 
|  |  | 
|  | centerCropBitmap(bitmap, imageView, new BitmapReceiver() { | 
|  | @Override | 
|  | public void onBitmapDecoded(@Nullable Bitmap newBitmap) { | 
|  | Drawable[] layers = new Drawable[2]; | 
|  | Drawable existingDrawable = imageView.getDrawable(); | 
|  |  | 
|  | if (existingDrawable instanceof TransitionDrawable) { | 
|  | // Take only the second layer in the existing TransitionDrawable so | 
|  | // we don't keep | 
|  | // around a reference to older layers which are no longer shown (this | 
|  | // way we avoid a | 
|  | // memory leak). | 
|  | TransitionDrawable existingTransitionDrawable = | 
|  | (TransitionDrawable) existingDrawable; | 
|  | int id = existingTransitionDrawable.getId(1); | 
|  | layers[0] = existingTransitionDrawable.findDrawableByLayerId(id); | 
|  | } else { | 
|  | layers[0] = existingDrawable; | 
|  | } | 
|  | layers[1] = new BitmapDrawable(resources, newBitmap); | 
|  |  | 
|  | TransitionDrawable transitionDrawable = new TransitionDrawable(layers); | 
|  | transitionDrawable.setCrossFadeEnabled(true); | 
|  |  | 
|  | imageView.setImageDrawable(transitionDrawable); | 
|  | transitionDrawable.startTransition(transitionDurationMillis); | 
|  |  | 
|  | if (drawableLoadedListener != null) { | 
|  | drawableLoadedListener.onDrawableLoaded(); | 
|  | } | 
|  | } | 
|  | }); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Loads the image for this asset into the provided ImageView which is used for the preview. | 
|  | * While waiting for the image to load, first loads a ColorDrawable based on the provided | 
|  | * placeholder color. | 
|  | * | 
|  | * @param activity         Activity hosting the ImageView. | 
|  | * @param imageView        ImageView which is the target view of this asset. | 
|  | * @param placeholderColor Color of placeholder set to ImageView while waiting for image to | 
|  | *                         load. | 
|  | * @param offsetToStart    true to let the preview show from the start of the image, false to | 
|  | *                         center-aligned to the image. | 
|  | */ | 
|  | public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, | 
|  | boolean offsetToStart) { | 
|  | loadPreviewImage(activity, imageView, placeholderColor, offsetToStart, null); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Loads the image for this asset into the provided ImageView which is used for the preview. | 
|  | * While waiting for the image to load, first loads a ColorDrawable based on the provided | 
|  | * placeholder color. | 
|  | * | 
|  | * @param activity         Activity hosting the ImageView. | 
|  | * @param imageView        ImageView which is the target view of this asset. | 
|  | * @param placeholderColor Color of placeholder set to ImageView while waiting for image to | 
|  | *                         load. | 
|  | * @param offsetToStart    true to let the preview show from the start of the image, false to | 
|  | *                         center-aligned to the image. | 
|  | * @param cropHints        A Map of display size to crop rect | 
|  | */ | 
|  | public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, | 
|  | boolean offsetToStart, @Nullable Map<Point, Rect> cropHints) { | 
|  | boolean needsTransition = imageView.getDrawable() == null; | 
|  | Drawable placeholderDrawable = new ColorDrawable(placeholderColor); | 
|  | if (needsTransition) { | 
|  | imageView.setImageDrawable(placeholderDrawable); | 
|  | } | 
|  |  | 
|  | decodeRawDimensions(activity, dimensions -> { | 
|  | // TODO (b/286404249): A proper fix here would be to find out why the | 
|  | //  leak happens in first place | 
|  | if (activity.isDestroyed()) { | 
|  | return; | 
|  | } | 
|  | if (dimensions == null) { | 
|  | loadDrawable(activity, imageView, placeholderColor); | 
|  | return; | 
|  | } | 
|  |  | 
|  | boolean isRtl = RtlUtils.isRtl(activity); | 
|  | Display defaultDisplay = activity.getWindowManager().getDefaultDisplay(); | 
|  | Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay); | 
|  | Rect visibleRawWallpaperRect = | 
|  | WallpaperCropUtils.calculateVisibleRect(dimensions, screenSize); | 
|  | if (cropHints != null && cropHints.containsKey(screenSize)) { | 
|  | visibleRawWallpaperRect = CropSizeUtil.INSTANCE.fitCropRectToLayoutDirection( | 
|  | cropHints.get(screenSize), screenSize, RtlUtils.isRtl(activity)); | 
|  | // For multi-crop, the visibleRawWallpaperRect above is already the exact size of | 
|  | // the part of wallpaper we should show on the screen, turning off the old RTL | 
|  | // logic by assigning false. | 
|  | isRtl = false; | 
|  | } | 
|  |  | 
|  | // TODO(b/264234793): Make offsetToStart general support or for the specific asset. | 
|  | adjustCropRect(activity, dimensions, visibleRawWallpaperRect, offsetToStart); | 
|  |  | 
|  | BitmapCropper bitmapCropper = InjectorProvider.getInjector().getBitmapCropper(); | 
|  | bitmapCropper.cropAndScaleBitmap(this, /* scale= */ 1f, visibleRawWallpaperRect, | 
|  | isRtl, | 
|  | new BitmapCropper.Callback() { | 
|  | @Override | 
|  | public void onBitmapCropped(Bitmap croppedBitmap) { | 
|  | // Since the size of the cropped bitmap may not exactly the same with | 
|  | // image view(maybe has 1px or 2px difference), | 
|  | // so set CENTER_CROP to let the bitmap to fit the image view. | 
|  | if (!activity.isDestroyed()) { | 
|  | imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); | 
|  | if (!needsTransition) { | 
|  | imageView.setImageBitmap(croppedBitmap); | 
|  | return; | 
|  | } | 
|  |  | 
|  | Resources resources = activity.getResources(); | 
|  |  | 
|  | Drawable[] layers = new Drawable[2]; | 
|  | layers[0] = placeholderDrawable; | 
|  | layers[1] = new BitmapDrawable(resources, croppedBitmap); | 
|  |  | 
|  | TransitionDrawable transitionDrawable = new | 
|  | TransitionDrawable(layers); | 
|  | transitionDrawable.setCrossFadeEnabled(true); | 
|  |  | 
|  | imageView.setImageDrawable(transitionDrawable); | 
|  | transitionDrawable.startTransition(resources.getInteger( | 
|  | android.R.integer.config_shortAnimTime)); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onError(@Nullable Throwable e) { | 
|  | if (!activity.isDestroyed()) { | 
|  | loadDrawable(activity, imageView, placeholderColor); | 
|  | } | 
|  | } | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Interface for receiving decoded Bitmaps. | 
|  | */ | 
|  | public interface BitmapReceiver { | 
|  |  | 
|  | /** | 
|  | * Called with a decoded Bitmap object or null if there was an error decoding the bitmap. | 
|  | */ | 
|  | void onBitmapDecoded(@Nullable Bitmap bitmap); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Interface for receiving raw asset dimensions. | 
|  | */ | 
|  | public interface DimensionsReceiver { | 
|  |  | 
|  | /** | 
|  | * Called with raw dimensions of asset or null if the asset is unable to decode the raw | 
|  | * dimensions. | 
|  | * | 
|  | * @param dimensions Dimensions as a Point where width is represented by "x" and height by | 
|  | *                   "y". | 
|  | */ | 
|  | void onDimensionsDecoded(@Nullable Point dimensions); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Interface for being notified when a drawable has been loaded. | 
|  | */ | 
|  | public interface DrawableLoadedListener { | 
|  | void onDrawableLoaded(); | 
|  | } | 
|  |  | 
|  | protected void adjustCropRect(Context context, Point assetDimensions, Rect cropRect, | 
|  | boolean offsetToStart) { | 
|  | WallpaperCropUtils.adjustCropRect(context, cropRect, true /* zoomIn */); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a copy of the given bitmap which is center cropped and scaled | 
|  | * to fit in the given ImageView and the thread runs on ExecutorService. | 
|  | */ | 
|  | public void centerCropBitmap(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver) { | 
|  | Point imageViewDimensions = getViewDimensions(view); | 
|  | sExecutorService.execute(() -> { | 
|  | int measuredWidth = imageViewDimensions.x; | 
|  | int measuredHeight = imageViewDimensions.y; | 
|  |  | 
|  | int bitmapWidth = bitmap.getWidth(); | 
|  | int bitmapHeight = bitmap.getHeight(); | 
|  |  | 
|  | float scale = Math.min( | 
|  | (float) bitmapWidth / measuredWidth, | 
|  | (float) bitmapHeight / measuredHeight); | 
|  |  | 
|  | Bitmap scaledBitmap = Bitmap.createScaledBitmap( | 
|  | bitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale), | 
|  | true); | 
|  |  | 
|  | int horizontalGutterPx = Math.max(0, (scaledBitmap.getWidth() - measuredWidth) / 2); | 
|  | int verticalGutterPx = Math.max(0, (scaledBitmap.getHeight() - measuredHeight) / 2); | 
|  | Bitmap result = Bitmap.createBitmap( | 
|  | scaledBitmap, | 
|  | horizontalGutterPx, | 
|  | verticalGutterPx, | 
|  | scaledBitmap.getWidth() - (2 * horizontalGutterPx), | 
|  | scaledBitmap.getHeight() - (2 * verticalGutterPx)); | 
|  | decodeBitmapCompleted(bitmapReceiver, result); | 
|  | }); | 
|  | } | 
|  | } |