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