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