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