• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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