1 /* 2 * Copyright (C) 2023 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 17 package com.android.server.wallpaper; 18 19 import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; 20 import static android.app.WallpaperManager.getOrientation; 21 import static android.app.WallpaperManager.getRotatedOrientation; 22 import static android.view.Display.DEFAULT_DISPLAY; 23 24 import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE; 25 import static com.android.server.wallpaper.WallpaperUtils.RECORD_LOCK_FILE; 26 import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER; 27 import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; 28 import static com.android.window.flags.Flags.multiCrop; 29 30 import android.graphics.Bitmap; 31 import android.graphics.BitmapFactory; 32 import android.graphics.ImageDecoder; 33 import android.graphics.Point; 34 import android.graphics.Rect; 35 import android.os.FileUtils; 36 import android.os.SELinux; 37 import android.text.TextUtils; 38 import android.util.Slog; 39 import android.util.SparseArray; 40 import android.view.DisplayInfo; 41 import android.view.View; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.server.utils.TimingsTraceAndSlog; 45 46 import libcore.io.IoUtils; 47 48 import java.io.BufferedOutputStream; 49 import java.io.File; 50 import java.io.FileOutputStream; 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Locale; 54 55 /** 56 * Helper file for wallpaper cropping 57 * Meant to have a single instance, only used internally by system_server 58 * @hide 59 */ 60 public class WallpaperCropper { 61 62 private static final String TAG = WallpaperCropper.class.getSimpleName(); 63 private static final boolean DEBUG = false; 64 private static final boolean DEBUG_CROP = true; 65 66 /** 67 * Maximum acceptable parallax. 68 * A value of 1 means "the additional width for parallax is at most 100% of the screen width" 69 */ 70 @VisibleForTesting static final float MAX_PARALLAX = 1f; 71 72 /** 73 * We define three ways to adjust a crop. These modes are used depending on the situation: 74 * - When going from unfolded to folded, we want to remove content 75 * - When going from folded to unfolded, we want to add content 76 * - For a screen rotation, we want to keep the same amount of content 77 */ 78 @VisibleForTesting static final int ADD = 1; 79 @VisibleForTesting static final int REMOVE = 2; 80 @VisibleForTesting static final int BALANCE = 3; 81 82 private final WallpaperDisplayHelper mWallpaperDisplayHelper; 83 84 /** 85 * Helpers exposed to the window manager part (WallpaperController) 86 */ 87 public interface WallpaperCropUtils { 88 89 /** 90 * Equivalent to {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)} 91 */ getCrop(Point displaySize, Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl)92 Rect getCrop(Point displaySize, Point bitmapSize, 93 SparseArray<Rect> suggestedCrops, boolean rtl); 94 } 95 WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper)96 WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) { 97 mWallpaperDisplayHelper = wallpaperDisplayHelper; 98 } 99 100 /** 101 * Given the dimensions of the original wallpaper image, some optional suggested crops 102 * (either defined by the user, or coming from a backup), and whether the device has RTL layout, 103 * generate a crop for the current display. This is done through the following process: 104 * <ul> 105 * <li> If no suggested crops are provided, in most cases render the full image left-aligned 106 * (or right-aligned if RTL) and use any additional width for parallax up to 107 * {@link #MAX_PARALLAX}. There are exceptions, see comments in "Case 1" of this function. 108 * <li> If there is a suggested crop the given displaySize, reuse the suggested crop and 109 * adjust it using {@link #getAdjustedCrop}. 110 * <li> If there are suggested crops, but not for the orientation of the given displaySize, 111 * reuse one of the suggested crop for another orientation and adjust if using 112 * {@link #getAdjustedCrop}. 113 * </ul> 114 * 115 * @param displaySize The dimensions of the surface where we want to render the wallpaper 116 * @param bitmapSize The dimensions of the wallpaper bitmap 117 * @param rtl Whether the device is right-to-left 118 * @param suggestedCrops An optional list of user-defined crops for some orientations. 119 * If there is a suggested crop for 120 * 121 * @return A Rect indicating how to crop the bitmap for the current display. 122 */ getCrop(Point displaySize, Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl)123 public Rect getCrop(Point displaySize, Point bitmapSize, 124 SparseArray<Rect> suggestedCrops, boolean rtl) { 125 126 int orientation = getOrientation(displaySize); 127 128 // Case 1: if no crops are provided, show the full image (from the left, or right if RTL). 129 if (suggestedCrops == null || suggestedCrops.size() == 0) { 130 Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); 131 132 // The first exception is if the device is a foldable and we're on the folded screen. 133 // In that case, show the center of what's on the unfolded screen. 134 int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); 135 if (unfoldedOrientation != ORIENTATION_UNKNOWN) { 136 // Let the system know that we're showing the full image on the unfolded screen 137 SparseArray<Rect> newSuggestedCrops = new SparseArray<>(); 138 newSuggestedCrops.put(unfoldedOrientation, crop); 139 // This will fall into "Case 4" of this function and center the folded screen 140 return getCrop(displaySize, bitmapSize, newSuggestedCrops, rtl); 141 } 142 143 // The second exception is if we're on tablet and we're on portrait mode. 144 // In that case, center the wallpaper relatively to landscape and put some parallax. 145 boolean isTablet = mWallpaperDisplayHelper.isLargeScreen() 146 && !mWallpaperDisplayHelper.isFoldable(); 147 if (isTablet && displaySize.x < displaySize.y) { 148 Point rotatedDisplaySize = new Point(displaySize.y, displaySize.x); 149 // compute the crop on landscape (without parallax) 150 Rect landscapeCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); 151 landscapeCrop = noParallax(landscapeCrop, rotatedDisplaySize, bitmapSize, rtl); 152 // compute the crop on portrait at the center of the landscape crop 153 crop = getAdjustedCrop(landscapeCrop, bitmapSize, displaySize, false, rtl, ADD); 154 155 // add some parallax (until the border of the landscape crop without parallax) 156 if (rtl) { 157 crop.left = landscapeCrop.left; 158 } else { 159 crop.right = landscapeCrop.right; 160 } 161 } 162 163 return getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD); 164 } 165 166 // If any suggested crop is invalid, fallback to case 1 167 for (int i = 0; i < suggestedCrops.size(); i++) { 168 Rect testCrop = suggestedCrops.valueAt(i); 169 if (testCrop == null || testCrop.left < 0 || testCrop.top < 0 170 || testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) { 171 Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize); 172 return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); 173 } 174 } 175 176 // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop 177 Rect suggestedCrop = suggestedCrops.get(orientation); 178 if (suggestedCrop != null) { 179 return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, rtl, ADD); 180 } 181 182 // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and 183 // trying to preserve the zoom level and the center of the image 184 SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); 185 int rotatedOrientation = getRotatedOrientation(orientation); 186 suggestedCrop = suggestedCrops.get(rotatedOrientation); 187 Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); 188 if (suggestedCrop != null) { 189 // only keep the visible part (without parallax) 190 Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); 191 return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, BALANCE); 192 } 193 194 // Case 4: if the device is a foldable, if we're looking for a folded orientation and have 195 // the suggested crop of the relative unfolded orientation, reuse it by removing content. 196 int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); 197 suggestedCrop = suggestedCrops.get(unfoldedOrientation); 198 suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation); 199 if (suggestedCrop != null) { 200 // compute the visible part (without parallax) of the unfolded screen 201 Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); 202 // compute the folded crop, at the center of the crop of the unfolded screen 203 Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE); 204 // if we removed some width, add it back to add a parallax effect 205 if (res.width() < adjustedCrop.width()) { 206 if (rtl) res.left = Math.min(res.left, adjustedCrop.left); 207 else res.right = Math.max(res.right, adjustedCrop.right); 208 // use getAdjustedCrop(parallax=true) to make sure we don't exceed MAX_PARALLAX 209 res = getAdjustedCrop(res, bitmapSize, displaySize, true, rtl, ADD); 210 } 211 return res; 212 } 213 214 215 // Case 5: if the device is a foldable, if we're looking for an unfolded orientation and 216 // have the suggested crop of the relative folded orientation, reuse it by adding content. 217 int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation); 218 suggestedCrop = suggestedCrops.get(foldedOrientation); 219 suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation); 220 if (suggestedCrop != null) { 221 // only keep the visible part (without parallax) 222 Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); 223 return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, ADD); 224 } 225 226 // Case 6: for a foldable device, try to combine case 3 + case 4 or 5: 227 // rotate, then fold or unfold 228 Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); 229 if (rotatedDisplaySize != null) { 230 int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation); 231 int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation); 232 for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) { 233 suggestedCrop = suggestedCrops.get(suggestedOrientation); 234 if (suggestedCrop != null) { 235 Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); 236 SparseArray<Rect> rotatedCropMap = new SparseArray<>(); 237 rotatedCropMap.put(rotatedOrientation, rotatedCrop); 238 return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl); 239 } 240 } 241 } 242 243 // Case 7: could not properly reuse the suggested crops. Fall back to case 1. 244 Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize 245 + ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops 246 + ", orientation: " + orientation + ", rtl: " + rtl 247 + ", defaultDisplaySizes: " + defaultDisplaySizes); 248 return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); 249 } 250 251 /** 252 * Given a crop, a displaySize for the orientation of that crop, compute the visible part of the 253 * crop. This removes any additional width used for parallax. No-op if displaySize == null. 254 */ 255 @VisibleForTesting noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl)256 static Rect noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl) { 257 if (displaySize == null) return crop; 258 Rect adjustedCrop = getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD); 259 // only keep the visible part (without parallax) 260 float suggestedDisplayRatio = 1f * displaySize.x / displaySize.y; 261 int widthToRemove = (int) (adjustedCrop.width() 262 - (((float) adjustedCrop.height()) * suggestedDisplayRatio) + 0.5f); 263 if (rtl) { 264 adjustedCrop.left += widthToRemove; 265 } else { 266 adjustedCrop.right -= widthToRemove; 267 } 268 return adjustedCrop; 269 } 270 271 /** 272 * Adjust a given crop: 273 * <ul> 274 * <li>If parallax = true, make sure we have a parallax of at most {@link #MAX_PARALLAX}, 275 * by removing content from the right (or left if RTL) if necessary. 276 * <li>If parallax = false, make sure we do not have additional width for parallax. If we 277 * have additional width for parallax, remove half of the additional width on both sides. 278 * <li>Make sure the crop fills the screen, i.e. that the width/height ratio of the crop 279 * is at least the width/height ratio of the screen. This is done accordingly to the 280 * {@code mode} used, which can be either {@link #ADD}, {@link #REMOVE} or {@link #BALANCE}. 281 * </ul> 282 */ 283 @VisibleForTesting getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize, boolean parallax, boolean rtl, int mode)284 static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize, 285 boolean parallax, boolean rtl, int mode) { 286 Rect adjustedCrop = new Rect(crop); 287 float cropRatio = ((float) crop.width()) / crop.height(); 288 float screenRatio = ((float) screenSize.x) / screenSize.y; 289 if (cropRatio == screenRatio) return crop; 290 if (cropRatio > screenRatio) { 291 if (!parallax) { 292 // rotate everything 90 degrees clockwise, compute the result, and rotate back 293 int newLeft = bitmapSize.y - crop.bottom; 294 int newRight = newLeft + crop.height(); 295 int newTop = crop.left; 296 int newBottom = newTop + crop.width(); 297 Rect rotatedCrop = new Rect(newLeft, newTop, newRight, newBottom); 298 Point rotatedBitmap = new Point(bitmapSize.y, bitmapSize.x); 299 Point rotatedScreen = new Point(screenSize.y, screenSize.x); 300 Rect rect = getAdjustedCrop( 301 rotatedCrop, rotatedBitmap, rotatedScreen, false, rtl, mode); 302 int resultLeft = rect.top; 303 int resultRight = resultLeft + rect.height(); 304 int resultTop = rotatedBitmap.x - rect.right; 305 int resultBottom = resultTop + rect.width(); 306 return new Rect(resultLeft, resultTop, resultRight, resultBottom); 307 } 308 float additionalWidthForParallax = cropRatio / screenRatio - 1f; 309 if (additionalWidthForParallax > MAX_PARALLAX) { 310 int widthToRemove = (int) Math.ceil( 311 (additionalWidthForParallax - MAX_PARALLAX) * screenRatio * crop.height()); 312 if (rtl) { 313 adjustedCrop.left += widthToRemove; 314 } else { 315 adjustedCrop.right -= widthToRemove; 316 } 317 } 318 } else { 319 // Note: the third case when MODE == BALANCE, -W + sqrt(W * H * R), is the width to add 320 // so that, when removing the appropriate height, we get a bitmap of aspect ratio R and 321 // total surface of W * H. In other words it is the width to add to get the desired 322 // aspect ratio R, while preserving the total number of pixels W * H. 323 int widthToAdd = mode == REMOVE ? 0 324 : mode == ADD ? (int) (crop.height() * screenRatio - crop.width()) 325 : (int) (-crop.width() + Math.sqrt(crop.width() * crop.height() * screenRatio)); 326 int availableWidth = bitmapSize.x - crop.width(); 327 if (availableWidth >= widthToAdd) { 328 int widthToAddLeft = widthToAdd / 2; 329 int widthToAddRight = widthToAdd / 2 + widthToAdd % 2; 330 331 if (crop.left < widthToAddLeft) { 332 widthToAddRight += (widthToAddLeft - crop.left); 333 widthToAddLeft = crop.left; 334 } else if (bitmapSize.x - crop.right < widthToAddRight) { 335 widthToAddLeft += (widthToAddRight - (bitmapSize.x - crop.right)); 336 widthToAddRight = bitmapSize.x - crop.right; 337 } 338 adjustedCrop.left -= widthToAddLeft; 339 adjustedCrop.right += widthToAddRight; 340 } else { 341 adjustedCrop.left = 0; 342 adjustedCrop.right = bitmapSize.x; 343 } 344 int heightToRemove = (int) (crop.height() - (adjustedCrop.width() / screenRatio)); 345 adjustedCrop.top += heightToRemove / 2 + heightToRemove % 2; 346 adjustedCrop.bottom -= heightToRemove / 2; 347 } 348 return adjustedCrop; 349 } 350 351 /** 352 * To find the smallest sub-image that contains all the given crops. 353 * This is used in {@link #generateCrop(WallpaperData)} 354 * to determine how the file from {@link WallpaperData#getCropFile()} needs to be cropped. 355 * 356 * @param crops a list of rectangles 357 * @return the smallest rectangle that contains them all. 358 */ getTotalCrop(SparseArray<Rect> crops)359 public static Rect getTotalCrop(SparseArray<Rect> crops) { 360 int left = Integer.MAX_VALUE, top = Integer.MAX_VALUE; 361 int right = Integer.MIN_VALUE, bottom = Integer.MIN_VALUE; 362 for (int i = 0; i < crops.size(); i++) { 363 Rect rect = crops.valueAt(i); 364 left = Math.min(left, rect.left); 365 top = Math.min(top, rect.top); 366 right = Math.max(right, rect.right); 367 bottom = Math.max(bottom, rect.bottom); 368 } 369 return new Rect(left, top, right, bottom); 370 } 371 372 /** 373 * The crops stored in {@link WallpaperData#mCropHints} are relative to the original image. 374 * This computes the crops relative to the sub-image that will actually be rendered on a window. 375 */ getRelativeCropHints(WallpaperData wallpaper)376 SparseArray<Rect> getRelativeCropHints(WallpaperData wallpaper) { 377 SparseArray<Rect> result = new SparseArray<>(); 378 for (int i = 0; i < wallpaper.mCropHints.size(); i++) { 379 Rect adjustedRect = new Rect(wallpaper.mCropHints.valueAt(i)); 380 adjustedRect.offset(-wallpaper.cropHint.left, -wallpaper.cropHint.top); 381 adjustedRect.scale(1f / wallpaper.mSampleSize); 382 result.put(wallpaper.mCropHints.keyAt(i), adjustedRect); 383 } 384 return result; 385 } 386 387 /** 388 * Inverse operation of {@link #getRelativeCropHints} 389 */ getOriginalCropHints( WallpaperData wallpaper, List<Rect> relativeCropHints)390 static List<Rect> getOriginalCropHints( 391 WallpaperData wallpaper, List<Rect> relativeCropHints) { 392 List<Rect> result = new ArrayList<>(); 393 for (Rect crop : relativeCropHints) { 394 Rect originalRect = new Rect(crop); 395 originalRect.scale(wallpaper.mSampleSize); 396 originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.top); 397 result.add(originalRect); 398 } 399 return result; 400 } 401 402 /** 403 * Given some suggested crops, find cropHints for all orientations of the default display. 404 */ getDefaultCrops(SparseArray<Rect> suggestedCrops, Point bitmapSize)405 SparseArray<Rect> getDefaultCrops(SparseArray<Rect> suggestedCrops, Point bitmapSize) { 406 407 // If the suggested crops is single-element map with (ORIENTATION_UNKNOWN, cropHint), 408 // Crop the bitmap using the cropHint and compute the crops for cropped bitmap. 409 Rect cropHint = suggestedCrops.get(ORIENTATION_UNKNOWN); 410 if (cropHint != null) { 411 Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); 412 if (suggestedCrops.size() != 1 || !bitmapRect.contains(cropHint)) { 413 Slog.w(TAG, "Couldn't get default crops from suggested crops " + suggestedCrops 414 + " for bitmap of size " + bitmapSize + "; ignoring suggested crops"); 415 return getDefaultCrops(new SparseArray<>(), bitmapSize); 416 } 417 Point cropSize = new Point(cropHint.width(), cropHint.height()); 418 SparseArray<Rect> relativeDefaultCrops = getDefaultCrops(new SparseArray<>(), cropSize); 419 for (int i = 0; i < relativeDefaultCrops.size(); i++) { 420 relativeDefaultCrops.valueAt(i).offset(cropHint.left, cropHint.top); 421 } 422 return relativeDefaultCrops; 423 } 424 425 SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); 426 boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) 427 == View.LAYOUT_DIRECTION_RTL; 428 429 // adjust existing entries for the default display 430 SparseArray<Rect> adjustedSuggestedCrops = new SparseArray<>(); 431 for (int i = 0; i < defaultDisplaySizes.size(); i++) { 432 int orientation = defaultDisplaySizes.keyAt(i); 433 Point displaySize = defaultDisplaySizes.valueAt(i); 434 Rect suggestedCrop = suggestedCrops.get(orientation); 435 if (suggestedCrop != null) { 436 adjustedSuggestedCrops.put(orientation, 437 getCrop(displaySize, bitmapSize, suggestedCrops, rtl)); 438 } 439 } 440 441 // add missing cropHints for all orientation of the default display 442 SparseArray<Rect> result = adjustedSuggestedCrops.clone(); 443 for (int i = 0; i < defaultDisplaySizes.size(); i++) { 444 int orientation = defaultDisplaySizes.keyAt(i); 445 if (result.contains(orientation)) continue; 446 Point displaySize = defaultDisplaySizes.valueAt(i); 447 Rect newCrop = getCrop(displaySize, bitmapSize, adjustedSuggestedCrops, rtl); 448 result.put(orientation, newCrop); 449 } 450 return result; 451 } 452 453 /** 454 * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped 455 * for display. This will generate the crop and write it in the file. 456 */ generateCrop(WallpaperData wallpaper)457 void generateCrop(WallpaperData wallpaper) { 458 TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); 459 t.traceBegin("WPMS.generateCrop"); 460 generateCropInternal(wallpaper); 461 t.traceEnd(); 462 } 463 generateCropInternal(WallpaperData wallpaper)464 private void generateCropInternal(WallpaperData wallpaper) { 465 boolean success = false; 466 467 // Only generate crop for default display. 468 final WallpaperDisplayHelper.DisplayData wpData = 469 mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); 470 final DisplayInfo displayInfo = mWallpaperDisplayHelper.getDisplayInfo(DEFAULT_DISPLAY); 471 472 // Analyse the source; needed in multiple cases 473 BitmapFactory.Options options = new BitmapFactory.Options(); 474 options.inJustDecodeBounds = true; 475 BitmapFactory.decodeFile(wallpaper.getWallpaperFile().getAbsolutePath(), options); 476 if (options.outWidth <= 0 || options.outHeight <= 0) { 477 Slog.w(TAG, "Invalid wallpaper data"); 478 } else { 479 boolean needCrop = false; 480 boolean needScale; 481 482 Point bitmapSize = new Point(options.outWidth, options.outHeight); 483 Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); 484 485 if (multiCrop()) { 486 // Check that the suggested crops per screen orientation are all within the bitmap. 487 for (int i = 0; i < wallpaper.mCropHints.size(); i++) { 488 int orientation = wallpaper.mCropHints.keyAt(i); 489 Rect crop = wallpaper.mCropHints.valueAt(i); 490 if (crop.isEmpty() || !bitmapRect.contains(crop)) { 491 Slog.w(TAG, "Invalid crop " + crop + " for orientation " + orientation 492 + " and bitmap size " + bitmapSize + "; clearing suggested crops."); 493 wallpaper.mCropHints.clear(); 494 wallpaper.cropHint.set(bitmapRect); 495 break; 496 } 497 } 498 } 499 final Rect cropHint; 500 final SparseArray<Rect> defaultCrops; 501 502 // A wallpaper with cropHints = Map.of(ORIENTATION_UNKNOWN, rect) is treated like 503 // a wallpaper with cropHints = null and cropHint = rect. 504 Rect tempCropHint = wallpaper.mCropHints.get(ORIENTATION_UNKNOWN); 505 if (multiCrop() && tempCropHint != null) { 506 wallpaper.cropHint.set(tempCropHint); 507 wallpaper.mCropHints.clear(); 508 } 509 if (multiCrop() && wallpaper.mCropHints.size() > 0) { 510 // Some suggested crops per screen orientation were provided, 511 // use them to compute the default crops for this device 512 defaultCrops = getDefaultCrops(wallpaper.mCropHints, bitmapSize); 513 // Adapt the provided crops to match the actual crops for the default display 514 SparseArray<Rect> updatedCropHints = new SparseArray<>(); 515 for (int i = 0; i < wallpaper.mCropHints.size(); i++) { 516 int orientation = wallpaper.mCropHints.keyAt(i); 517 Rect defaultCrop = defaultCrops.get(orientation); 518 if (defaultCrop != null) { 519 updatedCropHints.put(orientation, defaultCrop); 520 } 521 } 522 wallpaper.mCropHints = updatedCropHints; 523 524 // Finally, compute the cropHint based on the default crops 525 cropHint = getTotalCrop(defaultCrops); 526 wallpaper.cropHint.set(cropHint); 527 if (DEBUG) { 528 Slog.d(TAG, "Generated default crops for wallpaper: " + defaultCrops 529 + " based on suggested crops: " + wallpaper.mCropHints); 530 } 531 } else if (multiCrop()) { 532 // No crops per screen orientation were provided, but an overall cropHint may be 533 // defined in wallpaper.cropHint. Compute the default crops for the sub-image 534 // defined by the cropHint, then recompute the cropHint based on the default crops. 535 // If the cropHint is empty or invalid, ignore it and use the full image. 536 if (wallpaper.cropHint.isEmpty()) wallpaper.cropHint.set(bitmapRect); 537 if (!bitmapRect.contains(wallpaper.cropHint)) { 538 Slog.w(TAG, "Ignoring wallpaper.cropHint = " + wallpaper.cropHint 539 + "; not within the bitmap of size " + bitmapSize); 540 wallpaper.cropHint.set(bitmapRect); 541 } 542 Point cropSize = new Point(wallpaper.cropHint.width(), wallpaper.cropHint.height()); 543 defaultCrops = getDefaultCrops(new SparseArray<>(), cropSize); 544 cropHint = getTotalCrop(defaultCrops); 545 cropHint.offset(wallpaper.cropHint.left, wallpaper.cropHint.top); 546 wallpaper.cropHint.set(cropHint); 547 if (DEBUG) { 548 Slog.d(TAG, "Generated default crops for wallpaper: " + defaultCrops); 549 } 550 } else { 551 cropHint = new Rect(wallpaper.cropHint); 552 defaultCrops = null; 553 } 554 555 if (DEBUG) { 556 Slog.v(TAG, "Generating crop for new wallpaper(s): 0x" 557 + Integer.toHexString(wallpaper.mWhich) 558 + " to " + wallpaper.getCropFile().getName() 559 + " crop=(" + cropHint.width() + 'x' + cropHint.height() 560 + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')'); 561 } 562 563 // Empty crop means use the full image 564 if (!multiCrop() && cropHint.isEmpty()) { 565 cropHint.left = cropHint.top = 0; 566 cropHint.right = options.outWidth; 567 cropHint.bottom = options.outHeight; 568 } else if (!multiCrop()) { 569 // force the crop rect to lie within the measured bounds 570 int dx = cropHint.right > options.outWidth ? options.outWidth - cropHint.right : 0; 571 int dy = cropHint.bottom > options.outHeight 572 ? options.outHeight - cropHint.bottom : 0; 573 cropHint.offset(dx, dy); 574 575 // If the crop hint was larger than the image we just overshot. Patch things up. 576 if (cropHint.left < 0) { 577 cropHint.left = 0; 578 } 579 if (cropHint.top < 0) { 580 cropHint.top = 0; 581 } 582 } 583 584 // Don't bother cropping if what we're left with is identity 585 needCrop = (options.outHeight > cropHint.height() 586 || options.outWidth > cropHint.width()); 587 588 // scale if the crop height winds up not matching the recommended metrics 589 needScale = cropHint.height() > wpData.mHeight 590 || cropHint.height() > GLHelper.getMaxTextureSize() 591 || cropHint.width() > GLHelper.getMaxTextureSize(); 592 593 float sampleSize = Float.MAX_VALUE; 594 if (multiCrop()) { 595 // If all crops for all orientations have more width and height in pixel 596 // than the display for this orientation, downsample the image 597 for (int i = 0; i < defaultCrops.size(); i++) { 598 int orientation = defaultCrops.keyAt(i); 599 Rect crop = defaultCrops.valueAt(i); 600 Point displayForThisOrientation = mWallpaperDisplayHelper 601 .getDefaultDisplaySizes().get(orientation); 602 if (displayForThisOrientation == null) continue; 603 float sampleSizeForThisOrientation = Math.max(1f, Math.min( 604 crop.width() / displayForThisOrientation.x, 605 crop.height() / displayForThisOrientation.y)); 606 sampleSize = Math.min(sampleSize, sampleSizeForThisOrientation); 607 } 608 // If the total crop has more width or height than either the max texture size 609 // or twice the largest display dimension, downsample the image 610 int maxCropSize = Math.min( 611 2 * mWallpaperDisplayHelper.getDefaultDisplayLargestDimension(), 612 GLHelper.getMaxTextureSize()); 613 float minimumSampleSize = Math.max(1f, Math.max( 614 (float) cropHint.height() / maxCropSize, 615 (float) cropHint.width()) / maxCropSize); 616 sampleSize = Math.max(sampleSize, minimumSampleSize); 617 needScale = sampleSize > 1f; 618 } 619 620 //make sure screen aspect ratio is preserved if width is scaled under screen size 621 if (needScale && !multiCrop()) { 622 final float scaleByHeight = (float) wpData.mHeight / (float) cropHint.height(); 623 final int newWidth = (int) (cropHint.width() * scaleByHeight); 624 if (newWidth < displayInfo.logicalWidth) { 625 final float screenAspectRatio = 626 (float) displayInfo.logicalHeight / (float) displayInfo.logicalWidth; 627 cropHint.bottom = (int) (cropHint.width() * screenAspectRatio); 628 needCrop = true; 629 } 630 } 631 632 if (DEBUG_CROP) { 633 Slog.v(TAG, "crop: w=" + cropHint.width() + " h=" + cropHint.height()); 634 if (multiCrop()) Slog.v(TAG, "defaultCrops: " + defaultCrops); 635 if (!multiCrop()) Slog.v(TAG, "dims: w=" + wpData.mWidth + " h=" + wpData.mHeight); 636 Slog.v(TAG, "meas: w=" + options.outWidth + " h=" + options.outHeight); 637 Slog.v(TAG, "crop?=" + needCrop + " scale?=" + needScale); 638 } 639 640 if (!needCrop && !needScale) { 641 // Simple case: the nominal crop fits what we want, so we take 642 // the whole thing and just copy the image file directly. 643 644 // TODO: It is not accurate to estimate bitmap size without decoding it, 645 // may be we can try to remove this optimized way in the future, 646 // that means, we will always go into the 'else' block. 647 648 success = FileUtils.copyFile(wallpaper.getWallpaperFile(), wallpaper.getCropFile()); 649 650 if (!success) { 651 wallpaper.getCropFile().delete(); 652 } 653 654 if (DEBUG) { 655 long estimateSize = (long) options.outWidth * options.outHeight * 4; 656 Slog.v(TAG, "Null crop of new wallpaper, estimate size=" 657 + estimateSize + ", success=" + success); 658 } 659 } else { 660 // Fancy case: crop and scale. First, we decode and scale down if appropriate. 661 FileOutputStream f = null; 662 BufferedOutputStream bos = null; 663 try { 664 // This actually downsamples only by powers of two, but that's okay; we do 665 // a proper scaling a bit later. This is to minimize transient RAM use. 666 // We calculate the largest power-of-two under the actual ratio rather than 667 // just let the decode take care of it because we also want to remap where the 668 // cropHint rectangle lies in the decoded [super]rect. 669 final int actualScale = cropHint.height() / wpData.mHeight; 670 int scale = 1; 671 while (2 * scale <= actualScale) { 672 scale *= 2; 673 } 674 options.inSampleSize = scale; 675 options.inJustDecodeBounds = false; 676 677 final Rect estimateCrop = new Rect(cropHint); 678 if (!multiCrop()) estimateCrop.scale(1f / options.inSampleSize); 679 else estimateCrop.scale(1f / sampleSize); 680 float hRatio = (float) wpData.mHeight / estimateCrop.height(); 681 final int destHeight = (int) (estimateCrop.height() * hRatio); 682 final int destWidth = (int) (estimateCrop.width() * hRatio); 683 684 // We estimated an invalid crop, try to adjust the cropHint to get a valid one. 685 if (!multiCrop() && destWidth > GLHelper.getMaxTextureSize()) { 686 if (DEBUG) { 687 Slog.w(TAG, "Invalid crop dimensions, trying to adjust."); 688 } 689 690 int newHeight = (int) (wpData.mHeight / hRatio); 691 int newWidth = (int) (wpData.mWidth / hRatio); 692 693 estimateCrop.set(cropHint); 694 estimateCrop.left += (cropHint.width() - newWidth) / 2; 695 estimateCrop.top += (cropHint.height() - newHeight) / 2; 696 estimateCrop.right = estimateCrop.left + newWidth; 697 estimateCrop.bottom = estimateCrop.top + newHeight; 698 cropHint.set(estimateCrop); 699 estimateCrop.scale(1f / options.inSampleSize); 700 } 701 702 // We've got the safe cropHint; now we want to scale it properly to 703 // the desired rectangle. 704 // That's a height-biased operation: make it fit the hinted height. 705 final int safeHeight = !multiCrop() 706 ? (int) (estimateCrop.height() * hRatio + 0.5f) 707 : (int) (cropHint.height() / sampleSize + 0.5f); 708 final int safeWidth = !multiCrop() 709 ? (int) (estimateCrop.width() * hRatio + 0.5f) 710 : (int) (cropHint.width() / sampleSize + 0.5f); 711 712 if (DEBUG_CROP) { 713 Slog.v(TAG, "Decode parameters:"); 714 if (!multiCrop()) { 715 Slog.v(TAG, 716 " cropHint=" + cropHint + ", estimateCrop=" + estimateCrop); 717 Slog.v(TAG, " down sampling=" + options.inSampleSize 718 + ", hRatio=" + hRatio); 719 Slog.v(TAG, " dest=" + destWidth + "x" + destHeight); 720 } 721 if (multiCrop()) { 722 Slog.v(TAG, " cropHint=" + cropHint); 723 Slog.v(TAG, " sampleSize=" + sampleSize); 724 } 725 Slog.v(TAG, " targetSize=" + safeWidth + "x" + safeHeight); 726 Slog.v(TAG, " maxTextureSize=" + GLHelper.getMaxTextureSize()); 727 } 728 729 //Create a record file and will delete if ImageDecoder work well. 730 final String recordName = 731 (wallpaper.getWallpaperFile().getName().equals(WALLPAPER) 732 ? RECORD_FILE : RECORD_LOCK_FILE); 733 final File record = new File(getWallpaperDir(wallpaper.userId), recordName); 734 record.createNewFile(); 735 Slog.v(TAG, "record path =" + record.getPath() 736 + ", record name =" + record.getName()); 737 738 final ImageDecoder.Source srcData = 739 ImageDecoder.createSource(wallpaper.getWallpaperFile()); 740 final int finalScale = scale; 741 final int rescaledBitmapWidth = (int) (0.5f + bitmapSize.x / sampleSize); 742 final int rescaledBitmapHeight = (int) (0.5f + bitmapSize.y / sampleSize); 743 Bitmap cropped = ImageDecoder.decodeBitmap(srcData, (decoder, info, src) -> { 744 if (!multiCrop()) decoder.setTargetSampleSize(finalScale); 745 if (multiCrop()) { 746 decoder.setTargetSize(rescaledBitmapWidth, rescaledBitmapHeight); 747 } 748 decoder.setCrop(estimateCrop); 749 }); 750 751 record.delete(); 752 753 if (!multiCrop() && cropped == null) { 754 Slog.e(TAG, "Could not decode new wallpaper"); 755 } else { 756 // We are safe to create final crop with safe dimensions now. 757 final Bitmap finalCrop = multiCrop() ? cropped 758 : Bitmap.createScaledBitmap(cropped, safeWidth, safeHeight, true); 759 760 if (multiCrop()) { 761 wallpaper.mSampleSize = sampleSize; 762 } 763 764 if (DEBUG) { 765 Slog.v(TAG, "Final extract:"); 766 Slog.v(TAG, " dims: w=" + wpData.mWidth 767 + " h=" + wpData.mHeight); 768 Slog.v(TAG, " out: w=" + finalCrop.getWidth() 769 + " h=" + finalCrop.getHeight()); 770 } 771 772 f = new FileOutputStream(wallpaper.getCropFile()); 773 bos = new BufferedOutputStream(f, 32 * 1024); 774 finalCrop.compress(Bitmap.CompressFormat.PNG, 100, bos); 775 // don't rely on the implicit flush-at-close when noting success 776 bos.flush(); 777 success = true; 778 } 779 } catch (Exception e) { 780 Slog.e(TAG, "Error decoding crop", e); 781 } finally { 782 IoUtils.closeQuietly(bos); 783 IoUtils.closeQuietly(f); 784 } 785 } 786 } 787 788 if (!success) { 789 Slog.e(TAG, "Unable to apply new wallpaper"); 790 wallpaper.getCropFile().delete(); 791 wallpaper.mCropHints.clear(); 792 wallpaper.cropHint.set(0, 0, 0, 0); 793 wallpaper.mSampleSize = 1f; 794 } 795 796 if (wallpaper.getCropFile().exists()) { 797 boolean didRestorecon = SELinux.restorecon(wallpaper.getCropFile().getAbsoluteFile()); 798 if (DEBUG) { 799 Slog.v(TAG, "restorecon() of crop file returned " + didRestorecon); 800 } 801 } 802 } 803 } 804