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