diff options
| author | 2023-05-17 15:24:53 +0000 | |
|---|---|---|
| committer | 2024-01-19 14:08:01 +0000 | |
| commit | d9915e4a91d7c9b2a232134e4518f929eb6880a0 (patch) | |
| tree | 33a5a0eaf7b6e9ad7be366f82ee4f15ce69c480a | |
| parent | ae30e283781894e19841d26f3805001c0c5f24d8 (diff) | |
New methods setBitmap, setStream, getBitmapCrop, getWallpaperColors
See go/wallpaper-multi-crop
Flag: ACONFIG com.android.window.flags.multi_crop DEVELOPMENT
Bug: 270726737
Test: atest WallpaperManagerTest
Test: atest WallpaperControllerTests
Test: atest WallpaperBackupAgentTest
Test: quite a lot of manual testing
Change-Id: Ic0bb0bd450c02aad01ea59fbfdaab415fb9838f9
16 files changed, 1397 insertions, 139 deletions
diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index d7d654672abc..5acb9b5cf9ae 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -16,6 +16,7 @@ package android.app; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.os.Bundle; @@ -26,6 +27,8 @@ import android.app.WallpaperInfo; import android.content.ComponentName; import android.app.WallpaperColors; +import java.util.List; + /** @hide */ interface IWallpaperManager { @@ -39,15 +42,21 @@ interface IWallpaperManager { * FLAG_SET_SYSTEM * FLAG_SET_LOCK * - * A 'null' cropHint rectangle is explicitly permitted as a sentinel for "whatever - * the source image's bounding rect is." + * 'screenOrientations' and 'crops' define how the wallpaper will be positioned for + * different screen orientations. If some screen orientations are missing, crops for these + * orientations will be added by the system. + * + * If 'screenOrientations' is null, 'crops' can be null or a singleton list. The system will + * fit the provided crop (or the whole image, if 'crops' is 'null') for the current device + * orientation, and add crops for the missing orientations. * * The completion callback's "onWallpaperChanged()" method is invoked when the * new wallpaper content is ready to display. */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.SET_WALLPAPER)") ParcelFileDescriptor setWallpaper(String name, in String callingPackage, - in Rect cropHint, boolean allowBackup, out Bundle extras, int which, - IWallpaperManagerCallback completion, int userId); + in int[] screenOrientations, in List<Rect> crops, boolean allowBackup, + out Bundle extras, int which, IWallpaperManagerCallback completion, int userId); /** * Set the live wallpaper. @@ -78,6 +87,30 @@ interface IWallpaperManager { boolean getCropped); /** + * For a given user and a list of display sizes, get a list of Rect representing the + * area of the current wallpaper that is displayed for each display size. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL)") + @SuppressWarnings(value={"untyped-collection"}) + List getBitmapCrops(in List<Point> displaySizes, int which, boolean originalBitmap, int userId); + + /** + * Return how a bitmap of a given size would be cropped for a given list of display sizes when + * set with the given suggested crops. + * @hide + */ + @SuppressWarnings(value={"untyped-collection"}) + List getFutureBitmapCrops(in Point bitmapSize, in List<Point> displaySizes, + in int[] screenOrientations, in List<Rect> crops); + + /** + * Return how a bitmap of a given size would be cropped when set with the given suggested crops. + * @hide + */ + @SuppressWarnings(value={"untyped-collection"}) + Rect getBitmapCrop(in Point bitmapSize, in int[] screenOrientations, in List<Rect> crops); + + /** * Retrieve the given user's current wallpaper ID of the given kind. */ int getWallpaperIdForUser(int which, int userId); @@ -245,11 +278,4 @@ interface IWallpaperManager { * @hide */ boolean isStaticWallpaper(int which); - - /** - * Temporary method for project b/270726737. - * Return true if the wallpaper supports different crops for different display dimensions. - * @hide - */ - boolean isMultiCropEnabled(); } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 62db90f79091..63f37f150d33 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -18,11 +18,14 @@ package android.app; import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_WALLPAPER_INTERNAL; +import static android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; +import static com.android.window.flags.Flags.FLAG_MULTI_CROP; import static com.android.window.flags.Flags.multiCrop; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; @@ -58,6 +61,7 @@ import android.graphics.ImageDecoder; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; @@ -84,6 +88,7 @@ import android.util.ArraySet; import android.util.Log; import android.util.MathUtils; import android.util.Pair; +import android.util.SparseArray; import android.view.Display; import android.view.WindowManagerGlobal; @@ -104,6 +109,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -289,6 +295,79 @@ public class WallpaperManager { public static final String EXTRA_FROM_FOREGROUND_APP = "android.service.wallpaper.extra.FROM_FOREGROUND_APP"; + /** + * The different screen orientations. {@link #getOrientation} provides their exact definition. + * This is only used internally by the framework and the WallpaperBackupAgent. + * @hide + */ + @IntDef(value = { + ORIENTATION_UNKNOWN, + PORTRAIT, + LANDSCAPE, + SQUARE_PORTRAIT, + SQUARE_LANDSCAPE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScreenOrientation {} + + /** + * @hide + */ + public static final int ORIENTATION_UNKNOWN = -1; + + /** + * Portrait orientation of most screens + * @hide + */ + public static final int PORTRAIT = 0; + + /** + * Landscape orientation of most screens + * @hide + */ + public static final int LANDSCAPE = 1; + + /** + * Portrait orientation with similar width and height (e.g. the inner screen of a foldable) + * @hide + */ + public static final int SQUARE_PORTRAIT = 2; + + /** + * Landscape orientation with similar width and height (e.g. the inner screen of a foldable) + * @hide + */ + public static final int SQUARE_LANDSCAPE = 3; + + /** + * Converts a (width, height) screen size to a {@link ScreenOrientation}. + * @param screenSize the dimensions of a screen + * @return the corresponding {@link ScreenOrientation}. + * @hide + */ + public static @ScreenOrientation int getOrientation(Point screenSize) { + float ratio = ((float) screenSize.x) / screenSize.y; + // ratios between 3/4 and 4/3 are considered square + return ratio >= 4 / 3f ? LANDSCAPE + : ratio > 1f ? SQUARE_LANDSCAPE + : ratio > 3 / 4f ? SQUARE_PORTRAIT + : PORTRAIT; + } + + /** + * Get the 90° rotation of a given orientation + * @hide + */ + public static @ScreenOrientation int getRotatedOrientation(@ScreenOrientation int orientation) { + switch (orientation) { + case PORTRAIT: return LANDSCAPE; + case LANDSCAPE: return PORTRAIT; + case SQUARE_PORTRAIT: return SQUARE_LANDSCAPE; + case SQUARE_LANDSCAPE: return SQUARE_PORTRAIT; + default: return ORIENTATION_UNKNOWN; + } + } + // flags for which kind of wallpaper to act on /** @hide */ @@ -867,15 +946,8 @@ public class WallpaperManager { * @hide */ public static boolean isMultiCropEnabled() { - if (sGlobals == null) { - sIsMultiCropEnabled = multiCrop(); - } if (sIsMultiCropEnabled == null) { - try { - sIsMultiCropEnabled = sGlobals.mService.isMultiCropEnabled(); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } + sIsMultiCropEnabled = multiCrop(); } return sIsMultiCropEnabled; } @@ -1502,6 +1574,99 @@ public class WallpaperManager { } /** + * For the current user, given a list of display sizes, return a list of rectangles representing + * the area of the current wallpaper that would be shown for each of these sizes. + * + * @param displaySizes the display sizes. + * @param which wallpaper type. Must be either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. + * @param originalBitmap If true, return areas relative to the original bitmap. + * If false, return areas relative to the cropped bitmap. + * @return A List of Rect where the Rect is within the cropped/original bitmap, and corresponds + * to what is displayed. The Rect may have a larger width/height ratio than the screen + * due to parallax. Return {@code null} if the wallpaper is not an ImageWallpaper. + * Also return {@code null} when called with which={@link #FLAG_LOCK} if there is a + * shared home + lock wallpaper. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(READ_WALLPAPER_INTERNAL) + @Nullable + public List<Rect> getBitmapCrops(@NonNull List<Point> displaySizes, + @SetWallpaperFlags int which, boolean originalBitmap) { + checkExactlyOneWallpaperFlagSet(which); + try { + return sGlobals.mService.getBitmapCrops(displaySizes, which, originalBitmap, + mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * For preview purposes. + * Return how a bitmap of a given size would be cropped for a given list of display sizes, if + * it was set as wallpaper via {@link #setBitmapWithCrops(Bitmap, Map, boolean, int)} or + * {@link #setStreamWithCrops(InputStream, Map, boolean, int)}. + * + * @return A List of Rect where the Rect is within the bitmap, and corresponds to what is + * displayed for each display size. The Rect may have a larger width/height ratio than + * the display due to parallax. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @Nullable + public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes, + @Nullable Map<Point, Rect> cropHints) { + try { + if (cropHints == null) cropHints = Map.of(); + Set<Map.Entry<Point, Rect>> entries = cropHints.entrySet(); + int[] screenOrientations = entries.stream().mapToInt(entry -> + getOrientation(entry.getKey())).toArray(); + List<Rect> crops = entries.stream().map(Map.Entry::getValue).toList(); + return sGlobals.mService.getFutureBitmapCrops(bitmapSize, displaySizes, + screenOrientations, crops); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * For preview purposes. + * Compute the wallpaper colors of the given bitmap, if it was set as wallpaper via + * {@link #setBitmapWithCrops(Bitmap, Map, boolean, int)} or + * {@link #setStreamWithCrops(InputStream, Map, boolean, int)}. + * Return {@code null} if an error occurred and the colors could not be computed. + * + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT) + @Nullable + public WallpaperColors getWallpaperColors(@NonNull Bitmap bitmap, + @Nullable Map<Point, Rect> cropHints) { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + throw new RuntimeException(new DeadSystemException()); + } + try { + if (cropHints == null) cropHints = Map.of(); + Set<Map.Entry<Point, Rect>> entries = cropHints.entrySet(); + int[] screenOrientations = entries.stream().mapToInt(entry -> + getOrientation(entry.getKey())).toArray(); + List<Rect> crops = entries.stream().map(Map.Entry::getValue).toList(); + Point bitmapSize = new Point(bitmap.getWidth(), bitmap.getHeight()); + Rect crop = sGlobals.mService.getBitmapCrop(bitmapSize, screenOrientations, crops); + float dimAmount = getWallpaperDimAmount(); + Bitmap croppedBitmap = Bitmap.createBitmap( + bitmap, crop.left, crop.top, crop.width(), crop.height()); + WallpaperColors result = WallpaperColors.fromBitmap(croppedBitmap, dimAmount); + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * <strong> Important note: </strong> * <ul> * <li>Up to version S, this method requires the @@ -1971,7 +2136,7 @@ public class WallpaperManager { /* Set the wallpaper to the default values */ ParcelFileDescriptor fd = sGlobals.mService.setWallpaper( "res:" + resources.getResourceName(resid), - mContext.getOpPackageName(), null, false, result, which, completion, + mContext.getOpPackageName(), null, null, false, result, which, completion, mContext.getUserId()); if (fd != null) { FileOutputStream fos = null; @@ -2089,6 +2254,11 @@ public class WallpaperManager { public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup, @SetWallpaperFlags int which, int userId) throws IOException { + if (multiCrop()) { + SparseArray<Rect> cropMap = new SparseArray<>(); + if (visibleCropHint != null) cropMap.put(ORIENTATION_UNKNOWN, visibleCropHint); + return setBitmapWithCrops(fullImage, cropMap, allowBackup, which, userId); + } validateRect(visibleCropHint); if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); @@ -2096,9 +2266,69 @@ public class WallpaperManager { } final Bundle result = new Bundle(); final WallpaperSetCompletion completion = new WallpaperSetCompletion(); + final List<Rect> crops = visibleCropHint == null ? null : List.of(visibleCropHint); try { ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, - mContext.getOpPackageName(), visibleCropHint, allowBackup, + mContext.getOpPackageName(), null, crops, allowBackup, result, which, + completion, userId); + if (fd != null) { + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + fullImage.compress(Bitmap.CompressFormat.PNG, 90, fos); + fos.close(); + completion.waitForCompletion(); + } finally { + IoUtils.closeQuietly(fos); + } + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0); + } + + /** + * Version of setBitmap that defines how the wallpaper will be positioned for different + * display sizes. + * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. + * @param cropHints map from screen dimensions to a sub-region of the image to display for those + * dimensions. The {@code Rect} sub-region may have a larger width/height ratio + * than the screen dimensions to apply a horizontal parallax effect. If the + * map is empty or some entries are missing, the system will apply a default + * strategy to position the wallpaper for any unspecified screen dimensions. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) + public int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull Map<Point, Rect> cropHints, + boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + SparseArray<Rect> crops = new SparseArray<>(); + cropHints.forEach((k, v) -> crops.put(getOrientation(k), v)); + return setBitmapWithCrops(fullImage, crops, allowBackup, which, mContext.getUserId()); + } + + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) + private int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull SparseArray<Rect> cropHints, + boolean allowBackup, @SetWallpaperFlags int which, int userId) throws IOException { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + throw new RuntimeException(new DeadSystemException()); + } + int size = cropHints.size(); + int[] screenOrientations = new int[size]; + List<Rect> crops = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + screenOrientations[i] = cropHints.keyAt(i); + Rect cropHint = cropHints.valueAt(i); + validateRect(cropHint); + crops.add(cropHint); + } + final Bundle result = new Bundle(); + final WallpaperSetCompletion completion = new WallpaperSetCompletion(); + try { + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, + mContext.getOpPackageName(), screenOrientations, crops, allowBackup, result, which, completion, userId); if (fd != null) { FileOutputStream fos = null; @@ -2214,6 +2444,11 @@ public class WallpaperManager { public int setStream(InputStream bitmapData, Rect visibleCropHint, boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + if (multiCrop()) { + SparseArray<Rect> cropMap = new SparseArray<>(); + if (visibleCropHint != null) cropMap.put(ORIENTATION_UNKNOWN, visibleCropHint); + return setStreamWithCrops(bitmapData, cropMap, allowBackup, which); + } validateRect(visibleCropHint); if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); @@ -2221,10 +2456,11 @@ public class WallpaperManager { } final Bundle result = new Bundle(); final WallpaperSetCompletion completion = new WallpaperSetCompletion(); + final List<Rect> crops = visibleCropHint == null ? null : List.of(visibleCropHint); try { ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, - mContext.getOpPackageName(), visibleCropHint, allowBackup, - result, which, completion, mContext.getUserId()); + mContext.getOpPackageName(), null, crops, allowBackup, result, which, + completion, mContext.getUserId()); if (fd != null) { FileOutputStream fos = null; try { @@ -2244,6 +2480,75 @@ public class WallpaperManager { } /** + * Version of setStream that defines how the wallpaper will be positioned for different + * display sizes. + * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. + * @param cropHints map from screen dimensions to a sub-region of the image to display for those + * dimensions. The {@code Rect} sub-region may have a larger width/height ratio + * than the screen dimensions to apply a horizontal parallax effect. If the + * map is empty or some entries are missing, the system will apply a default + * strategy to position the wallpaper for any unspecified screen dimensions. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) + public int setStreamWithCrops(InputStream bitmapData, @NonNull Map<Point, Rect> cropHints, + boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + SparseArray<Rect> crops = new SparseArray<>(); + cropHints.forEach((k, v) -> crops.put(getOrientation(k), v)); + return setStreamWithCrops(bitmapData, crops, allowBackup, which); + } + + /** + * Similar to {@link #setStreamWithCrops(InputStream, Map, boolean, int)}, but using + * {@link ScreenOrientation} as keys of the cropHints map. Used for backup & restore, since + * WallpaperBackupAgent stores orientations rather than the exact display size. + * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. + * @param cropHints map from {@link ScreenOrientation} to a sub-region of the image to display + * for that screen orientation. + * @hide + */ + @FlaggedApi(FLAG_MULTI_CROP) + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) + public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints, + boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + throw new RuntimeException(new DeadSystemException()); + } + int size = cropHints.size(); + int[] screenOrientations = new int[size]; + List<Rect> crops = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + screenOrientations[i] = cropHints.keyAt(i); + Rect cropHint = cropHints.valueAt(i); + validateRect(cropHint); + crops.add(cropHint); + } + final Bundle result = new Bundle(); + final WallpaperSetCompletion completion = new WallpaperSetCompletion(); + try { + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null, + mContext.getOpPackageName(), screenOrientations, crops, allowBackup, + result, which, completion, mContext.getUserId()); + if (fd != null) { + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + copyStreamToWallpaperFile(bitmapData, fos); + fos.close(); + completion.waitForCompletion(); + } finally { + IoUtils.closeQuietly(fos); + } + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0); + } + + /** * Return whether any users are currently set to use the wallpaper * with the given resource ID. That is, their wallpaper has been * set through {@link #setResource(int)} with the same resource id. @@ -2499,7 +2804,7 @@ public class WallpaperManager { * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) + @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT) public void setWallpaperDimAmount(@FloatRange (from = 0f, to = 1f) float dimAmount) { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); @@ -2519,7 +2824,7 @@ public class WallpaperManager { * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) + @RequiresPermission(SET_WALLPAPER_DIM_AMOUNT) public @FloatRange (from = 0f, to = 1f) float getWallpaperDimAmount() { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 3a778c314606..c77004d4eb17 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -4333,6 +4333,12 @@ "group": "WM_DEBUG_ANIM", "at": "com\/android\/server\/wm\/WindowStateAnimator.java" }, + "1810872941": { + "message": "setWallpaperCropHints: non-existent wallpaper token: %s", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "1820873642": { "message": "SyncGroup %d: Unfinished dependencies: %s", "level": "VERBOSE", diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java index 98421a9e1d3e..f31eb44f23f5 100644 --- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java +++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java @@ -18,11 +18,13 @@ package com.android.wallpaperbackup; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_INELIGIBLE; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_METADATA; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_WALLPAPER; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_QUOTA_EXCEEDED; +import static com.android.window.flags.Flags.multiCrop; import android.app.AppGlobals; import android.app.WallpaperManager; @@ -43,7 +45,9 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; +import android.util.Pair; import android.util.Slog; +import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; @@ -55,6 +59,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.util.List; /** * Backs up and restores wallpaper and metadata related to it. @@ -432,6 +437,27 @@ public class WallpaperBackupAgent extends BackupAgent { private void restoreFromStage(File stage, File info, String hintTag, int which) throws IOException { if (stage.exists()) { + if (multiCrop()) { + SparseArray<Rect> cropHints = parseCropHints(info, hintTag); + if (cropHints != null) { + Slog.i(TAG, "Got restored wallpaper; applying which=" + which + + "; cropHints = " + cropHints); + try (FileInputStream in = new FileInputStream(stage)) { + mWallpaperManager.setStreamWithCrops(in, cropHints, true, which); + } + // And log the success + if ((which & FLAG_SYSTEM) > 0) { + mEventLogger.onSystemImageWallpaperRestored(); + } + if ((which & FLAG_LOCK) > 0) { + mEventLogger.onLockImageWallpaperRestored(); + } + } else { + logRestoreError(which, ERROR_NO_METADATA); + } + return; + } + // Parse the restored info file to find the crop hint. Note that this currently // relies on a priori knowledge of the wallpaper info file schema. Rect cropHint = parseCropHint(info, hintTag); @@ -501,6 +527,47 @@ public class WallpaperBackupAgent extends BackupAgent { return cropHint; } + private SparseArray<Rect> parseCropHints(File wallpaperInfo, String sectionTag) { + SparseArray<Rect> cropHints = new SparseArray<>(); + try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { + XmlPullParser parser = Xml.resolvePullParser(stream); + int type; + do { + type = parser.next(); + if (type != XmlPullParser.START_TAG) continue; + String tag = parser.getName(); + if (!sectionTag.equals(tag)) continue; + for (Pair<Integer, String> pair: List.of( + new Pair<>(WallpaperManager.PORTRAIT, "Portrait"), + new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"), + new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"), + new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape"))) { + Rect cropHint = new Rect( + getAttributeInt(parser, "cropLeft" + pair.second, 0), + getAttributeInt(parser, "cropTop" + pair.second, 0), + getAttributeInt(parser, "cropRight" + pair.second, 0), + getAttributeInt(parser, "cropBottom" + pair.second, 0)); + if (!cropHint.isEmpty()) cropHints.put(pair.first, cropHint); + } + if (cropHints.size() == 0) { + // migration case: the crops per screen orientation are not specified. + // use the old attributes to restore the crop for one screen orientation. + Rect cropHint = new Rect( + getAttributeInt(parser, "cropLeft", 0), + getAttributeInt(parser, "cropTop", 0), + getAttributeInt(parser, "cropRight", 0), + getAttributeInt(parser, "cropBottom", 0)); + if (!cropHint.isEmpty()) cropHints.put(ORIENTATION_UNKNOWN, cropHint); + } + } while (type != XmlPullParser.END_DOCUMENT); + } catch (Exception e) { + // Whoops; can't process the info file at all. Report failure. + Slog.w(TAG, "Failed to parse restored crops: " + e.getMessage()); + return null; + } + return cropHints; + } + private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) { ComponentName name = null; try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java index fb521e11c083..053ed779a27a 100644 --- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java +++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java @@ -31,6 +31,7 @@ import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_IMG_LOC import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_IMG_SYSTEM; import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_LIVE_LOCK; import static com.android.wallpaperbackup.WallpaperEventLogger.WALLPAPER_LIVE_SYSTEM; +import static com.android.window.flags.Flags.multiCrop; import static com.google.common.truth.Truth.assertThat; @@ -60,6 +61,7 @@ import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.service.wallpaper.WallpaperService; +import android.util.SparseArray; import android.util.Xml; import androidx.test.InstrumentationRegistry; @@ -711,8 +713,13 @@ public class WallpaperBackupAgentTest { @Test public void testOnRestore_throwsException_logsErrors() throws Exception { - when(mWallpaperManager.setStream(any(), any(), anyBoolean(), anyInt())).thenThrow( - new RuntimeException()); + if (!multiCrop()) { + when(mWallpaperManager.setStream(any(), any(), anyBoolean(), anyInt())) + .thenThrow(new RuntimeException()); + } else { + when(mWallpaperManager.setStreamWithCrops(any(), any(SparseArray.class), anyBoolean(), + anyInt())).thenThrow(new RuntimeException()); + } mockStagedWallpaperFile(SYSTEM_WALLPAPER_STAGE); mockStagedWallpaperFile(WALLPAPER_INFO_STAGE); mWallpaperBackupAgent.onCreate(USER_HANDLE, BackupAnnotations.BackupDestination.CLOUD, diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index b773ade09b89..51acc8e01cda 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -16,21 +16,28 @@ package com.android.server.wallpaper; +import static android.app.WallpaperManager.getOrientation; +import static android.app.WallpaperManager.getRotatedOrientation; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE; import static com.android.server.wallpaper.WallpaperUtils.RECORD_LOCK_FILE; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER; import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; +import static com.android.window.flags.Flags.multiCrop; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageDecoder; +import android.graphics.Point; import android.graphics.Rect; import android.os.FileUtils; import android.os.SELinux; +import android.text.TextUtils; import android.util.Slog; +import android.util.SparseArray; import android.view.DisplayInfo; +import android.view.View; import com.android.server.utils.TimingsTraceAndSlog; @@ -39,28 +46,334 @@ import libcore.io.IoUtils; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; /** * Helper file for wallpaper cropping - * Meant to have a single instance, only used by the WallpaperManagerService + * Meant to have a single instance, only used internally by system_server + * @hide */ -class WallpaperCropper { +public class WallpaperCropper { private static final String TAG = WallpaperCropper.class.getSimpleName(); private static final boolean DEBUG = false; private static final boolean DEBUG_CROP = true; + /** + * Maximum acceptable parallax. + * A value of 1 means "the additional width for parallax is at most 100% of the screen width" + */ + private static final float MAX_PARALLAX = 1f; + + /** + * We define three ways to adjust a crop. These modes are used depending on the situation: + * - When going from unfolded to folded, we want to remove content + * - When going from folded to unfolded, we want to add content + * - For a screen rotation, we want to keep the same amount of content + */ + private static final int ADD = 1; + private static final int REMOVE = 2; + private static final int BALANCE = 3; + + private final WallpaperDisplayHelper mWallpaperDisplayHelper; + /** + * Helpers exposed to the window manager part (WallpaperController) + */ + public interface WallpaperCropUtils { + + /** + * Equivalent to {@link #getCrop(Point, Point, SparseArray, boolean)} + */ + Rect getCrop(Point displaySize, Point bitmapSize, + SparseArray<Rect> suggestedCrops, boolean rtl); + } + WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) { mWallpaperDisplayHelper = wallpaperDisplayHelper; } /** - * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped - * for display. + * Given the dimensions of the original wallpaper image, some optional suggested crops + * (either defined by the user, or coming from a backup), and whether the device is RTL, + * generate a crop for the current display. This is done through the following process: + * <ul> + * <li> If no suggested crops are provided, center the full image on the display. </li> + * <li> If there is a suggested crop the given displaySize, reuse the suggested crop and + * adjust it using {@link #getAdjustedCrop}. </li> + * <li> If there are suggested crops, but not for the orientation of the given displaySize, + * reuse one of the suggested crop for another orientation and adjust if using + * {@link #getAdjustedCrop}. </li> + * </ul> + * + * @param displaySize The dimensions of the surface where we want to render the wallpaper + * @param bitmapSize The dimensions of the wallpaper bitmap + * @param rtl Whether the device is right-to-left + * @param suggestedCrops An optional list of user-defined crops for some orientations. + * If there is a suggested crop for * - * This will generate the crop and write it in the file + * @return A Rect indicating how to crop the bitmap for the current display. + */ + public Rect getCrop(Point displaySize, Point bitmapSize, + SparseArray<Rect> suggestedCrops, boolean rtl) { + + // Case 1: if no crops are provided, center align the full image + if (suggestedCrops == null || suggestedCrops.size() == 0) { + Rect crop = new Rect(0, 0, displaySize.x, displaySize.y); + float scale = Math.min( + ((float) bitmapSize.x) / displaySize.x, + ((float) bitmapSize.y) / displaySize.y); + crop.scale(scale); + crop.offset((bitmapSize.x - crop.width()) / 2, + (bitmapSize.y - crop.height()) / 2); + return crop; + } + int orientation = getOrientation(displaySize); + + // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop + Rect suggestedCrop = suggestedCrops.get(orientation); + if (suggestedCrop != null) { + if (suggestedCrop.left < 0 || suggestedCrop.top < 0 + || suggestedCrop.right > bitmapSize.x || suggestedCrop.bottom > bitmapSize.y) { + Slog.w(TAG, "invalid suggested crop: " + suggestedCrop); + Rect fullImage = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + return getAdjustedCrop(fullImage, bitmapSize, displaySize, true, rtl, ADD); + } else { + return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, rtl, ADD); + } + } + + // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and + // trying to preserve the zoom level and the center of the image + SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); + int rotatedOrientation = getRotatedOrientation(orientation); + suggestedCrop = suggestedCrops.get(rotatedOrientation); + Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); + if (suggestedCrop != null) { + // only keep the visible part (without parallax) + Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); + return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, BALANCE); + } + + // Case 4: if the device is a foldable, if we're looking for a folded orientation and have + // the suggested crop of the relative unfolded orientation, reuse it by removing content. + int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); + suggestedCrop = suggestedCrops.get(unfoldedOrientation); + suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation); + if (suggestedCrop != null) { + // only keep the visible part (without parallax) + Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); + return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE); + } + + // Case 5: if the device is a foldable, if we're looking for an unfolded orientation and + // have the suggested crop of the relative folded orientation, reuse it by adding content. + int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation); + suggestedCrop = suggestedCrops.get(foldedOrientation); + suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation); + if (suggestedCrop != null) { + // only keep the visible part (without parallax) + Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); + return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, ADD); + } + + // Case 6: for a foldable device, try to combine case 3 + case 4 or 5: + // rotate, then fold or unfold + Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); + if (rotatedDisplaySize != null) { + int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation); + int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation); + for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) { + suggestedCrop = suggestedCrops.get(suggestedOrientation); + if (suggestedCrop != null) { + Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); + SparseArray<Rect> rotatedCropMap = new SparseArray<>(); + rotatedCropMap.put(rotatedOrientation, rotatedCrop); + return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl); + } + } + } + + // Case 7: could not properly reuse the suggested crops. Fall back to case 1. + Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize + + ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops + + ", orientation: " + orientation + ", rtl: " + rtl + + ", defaultDisplaySizes: " + defaultDisplaySizes); + return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); + } + + /** + * Given a crop, a displaySize for the orientation of that crop, compute the visible part of the + * crop. This removes any additional width used for parallax. No-op if displaySize == null. + */ + private static Rect noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl) { + if (displaySize == null) return crop; + Rect adjustedCrop = getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD); + // only keep the visible part (without parallax) + float suggestedDisplayRatio = 1f * displaySize.x / displaySize.y; + int widthToRemove = (int) (adjustedCrop.width() + - (((float) adjustedCrop.height()) * suggestedDisplayRatio) + 0.5f); + if (rtl) { + adjustedCrop.left += widthToRemove; + } else { + adjustedCrop.right -= widthToRemove; + } + return adjustedCrop; + } + + /** + * Adjust a given crop: + * <ul> + * <li>If parallax = true, make sure we have a parallax of at most {@link #MAX_PARALLAX}, + * by removing content from the right (or left if RTL) if necessary. + * </li> + * <li>If parallax = false, make sure we do not have additional width for parallax. If we + * have additional width for parallax, remove half of the additional width on both sides. + * </li> + * <li>Make sure the crop fills the screen, i.e. that the width/height ratio of the crop + * is at least the width/height ratio of the screen. If it is less, add width to the crop + * (if possible on both sides) to fill the screen. If not enough width available, remove + * height to the crop. + * </li> + * </ul> + */ + private static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize, + boolean parallax, boolean rtl, int mode) { + Rect adjustedCrop = new Rect(crop); + float cropRatio = ((float) crop.width()) / crop.height(); + float screenRatio = ((float) screenSize.x) / screenSize.y; + if (cropRatio >= screenRatio) { + if (!parallax) { + // rotate everything 90 degrees clockwise, compute the result, and rotate back + int newLeft = bitmapSize.y - crop.bottom; + int newRight = newLeft + crop.height(); + int newTop = crop.left; + int newBottom = newTop + crop.width(); + Rect rotatedCrop = new Rect(newLeft, newTop, newRight, newBottom); + Point rotatedBitmap = new Point(bitmapSize.y, bitmapSize.x); + Point rotatedScreen = new Point(screenSize.y, screenSize.x); + Rect rect = getAdjustedCrop(rotatedCrop, rotatedBitmap, rotatedScreen, false, rtl, + mode); + int resultLeft = rect.top; + int resultRight = resultLeft + rect.height(); + int resultTop = rotatedBitmap.x - rect.right; + int resultBottom = resultTop + rect.width(); + return new Rect(resultLeft, resultTop, resultRight, resultBottom); + } + float additionalWidthForParallax = cropRatio / screenRatio - 1f; + if (additionalWidthForParallax > MAX_PARALLAX) { + int widthToRemove = (int) Math.ceil( + (additionalWidthForParallax - MAX_PARALLAX) * screenRatio * crop.height()); + if (rtl) { + adjustedCrop.left += widthToRemove; + } else { + adjustedCrop.right -= widthToRemove; + } + } + } else { + int widthToAdd = mode == REMOVE ? 0 + : mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width()) + : (int) (0.5 + crop.height() - crop.width()); + int availableWidth = bitmapSize.x - crop.width(); + if (availableWidth >= widthToAdd) { + int widthToAddLeft = widthToAdd / 2; + int widthToAddRight = widthToAdd / 2 + widthToAdd % 2; + + if (crop.left < widthToAddLeft) { + widthToAddRight += (widthToAddLeft - crop.left); + widthToAddLeft = crop.left; + } else if (bitmapSize.x - crop.right < widthToAddRight) { + widthToAddLeft += (widthToAddRight - (bitmapSize.x - crop.right)); + widthToAddRight = bitmapSize.x - crop.right; + } + adjustedCrop.left -= widthToAddLeft; + adjustedCrop.right += widthToAddRight; + } else { + adjustedCrop.left = 0; + adjustedCrop.right = bitmapSize.x; + } + int heightToRemove = (int) (crop.height() - (adjustedCrop.width() / screenRatio)); + adjustedCrop.top += heightToRemove / 2 + heightToRemove % 2; + adjustedCrop.bottom -= heightToRemove / 2; + } + return adjustedCrop; + } + + /** + * To find the smallest sub-image that contains all the given crops. + * This is used in {@link #generateCrop(WallpaperData)} + * to determine how the file from {@link WallpaperData#getCropFile()} needs to be cropped. + * + * @param crops a list of rectangles + * @return the smallest rectangle that contains them all. + */ + public static Rect getTotalCrop(SparseArray<Rect> crops) { + int left = Integer.MAX_VALUE, top = Integer.MAX_VALUE; + int right = Integer.MIN_VALUE, bottom = Integer.MIN_VALUE; + for (int i = 0; i < crops.size(); i++) { + Rect rect = crops.valueAt(i); + left = Math.min(left, rect.left); + top = Math.min(top, rect.top); + right = Math.max(right, rect.right); + bottom = Math.max(bottom, rect.bottom); + } + return new Rect(left, top, right, bottom); + } + + /** + * The crops stored in {@link WallpaperData#mCropHints} are relative to the original image. + * This computes the crops relative to the sub-image that will actually be rendered on a window. + */ + SparseArray<Rect> getRelativeCropHints(WallpaperData wallpaper) { + SparseArray<Rect> result = new SparseArray<>(); + for (int i = 0; i < wallpaper.mCropHints.size(); i++) { + Rect adjustedRect = new Rect(wallpaper.mCropHints.valueAt(i)); + adjustedRect.offset(-wallpaper.cropHint.left, -wallpaper.cropHint.top); + adjustedRect.scale(1f / wallpaper.mSampleSize); + result.put(wallpaper.mCropHints.keyAt(i), adjustedRect); + } + return result; + } + + /** + * Inverse operation of {@link #getRelativeCropHints} + */ + static List<Rect> getOriginalCropHints( + WallpaperData wallpaper, List<Rect> relativeCropHints) { + List<Rect> result = new ArrayList<>(); + for (Rect crop : relativeCropHints) { + Rect originalRect = new Rect(crop); + originalRect.scale(wallpaper.mSampleSize); + originalRect.offset(wallpaper.cropHint.left, wallpaper.cropHint.right); + result.add(originalRect); + } + return result; + } + + /** + * Given some suggested crops, find cropHints for all orientations of the default display. + */ + SparseArray<Rect> getDefaultCrops(SparseArray<Rect> suggestedCrops, Point bitmapSize) { + SparseArray<Rect> result = new SparseArray<>(); + // add missing cropHints for all orientation of the default display + SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); + boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + for (int i = 0; i < defaultDisplaySizes.size(); i++) { + int orientation = defaultDisplaySizes.keyAt(i); + Point displaySize = defaultDisplaySizes.valueAt(i); + Rect newCrop = getCrop(displaySize, bitmapSize, suggestedCrops, rtl); + result.put(orientation, newCrop); + } + return result; + } + + /** + * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped + * for display. This will generate the crop and write it in the file. */ void generateCrop(WallpaperData wallpaper) { TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); @@ -75,27 +388,47 @@ class WallpaperCropper { // Only generate crop for default display. final WallpaperDisplayHelper.DisplayData wpData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); - final Rect cropHint = new Rect(wallpaper.cropHint); final DisplayInfo displayInfo = mWallpaperDisplayHelper.getDisplayInfo(DEFAULT_DISPLAY); - if (DEBUG) { - Slog.v(TAG, "Generating crop for new wallpaper(s): 0x" - + Integer.toHexString(wallpaper.mWhich) - + " to " + wallpaper.getCropFile().getName() - + " crop=(" + cropHint.width() + 'x' + cropHint.height() - + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')'); - } - // Analyse the source; needed in multiple cases BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(wallpaper.getWallpaperFile().getAbsolutePath(), options); if (options.outWidth <= 0 || options.outHeight <= 0) { Slog.w(TAG, "Invalid wallpaper data"); - success = false; } else { boolean needCrop = false; boolean needScale; + boolean multiCrop = multiCrop() && wallpaper.mSupportsMultiCrop; + + Point bitmapSize = new Point(options.outWidth, options.outHeight); + + final Rect cropHint; + if (multiCrop) { + SparseArray<Rect> defaultDisplayCrops = + getDefaultCrops(wallpaper.mCropHints, bitmapSize); + // adapt the entries in wallpaper.mCropHints for the actual display + SparseArray<Rect> updatedCropHints = new SparseArray<>(); + for (int i = 0; i < wallpaper.mCropHints.size(); i++) { + Rect defaultCrop = defaultDisplayCrops.valueAt(i); + if (defaultCrop != null) { + updatedCropHints.put(defaultDisplayCrops.keyAt(i), defaultCrop); + } + } + wallpaper.mCropHints = updatedCropHints; + cropHint = getTotalCrop(defaultDisplayCrops); + wallpaper.cropHint.set(cropHint); + } else { + cropHint = new Rect(wallpaper.cropHint); + } + + if (DEBUG) { + Slog.v(TAG, "Generating crop for new wallpaper(s): 0x" + + Integer.toHexString(wallpaper.mWhich) + + " to " + wallpaper.getCropFile().getName() + + " crop=(" + cropHint.width() + 'x' + cropHint.height() + + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')'); + } // Empty crop means use the full image if (cropHint.isEmpty()) { @@ -128,7 +461,7 @@ class WallpaperCropper { || cropHint.width() > GLHelper.getMaxTextureSize(); //make sure screen aspect ratio is preserved if width is scaled under screen size - if (needScale) { + if (needScale && !multiCrop) { final float scaleByHeight = (float) wpData.mHeight / (float) cropHint.height(); final int newWidth = (int) (cropHint.width() * scaleByHeight); if (newWidth < displayInfo.logicalWidth) { @@ -171,7 +504,7 @@ class WallpaperCropper { BufferedOutputStream bos = null; try { // This actually downsamples only by powers of two, but that's okay; we do - // a proper scaling blit later. This is to minimize transient RAM use. + // a proper scaling a bit later. This is to minimize transient RAM use. // We calculate the largest power-of-two under the actual ratio rather than // just let the decode take care of it because we also want to remap where the // cropHint rectangle lies in the decoded [super]rect. @@ -185,19 +518,31 @@ class WallpaperCropper { final Rect estimateCrop = new Rect(cropHint); estimateCrop.scale(1f / options.inSampleSize); - final float hRatio = (float) wpData.mHeight / estimateCrop.height(); + float hRatio = (float) wpData.mHeight / estimateCrop.height(); + if (multiCrop) { + // make sure the crop height is at most the display largest dimension + hRatio = (float) mWallpaperDisplayHelper.getDefaultDisplayLargestDimension() + / estimateCrop.height(); + hRatio = Math.min(hRatio, 1f); + } final int destHeight = (int) (estimateCrop.height() * hRatio); final int destWidth = (int) (estimateCrop.width() * hRatio); // We estimated an invalid crop, try to adjust the cropHint to get a valid one. if (destWidth > GLHelper.getMaxTextureSize()) { - int newHeight = (int) (wpData.mHeight / hRatio); - int newWidth = (int) (wpData.mWidth / hRatio); - if (DEBUG) { - Slog.v(TAG, "Invalid crop dimensions, trying to adjust."); + Slog.w(TAG, "Invalid crop dimensions, trying to adjust."); + } + if (multiCrop) { + // clear custom crop guidelines, fallback to system default + wallpaper.mCropHints.clear(); + generateCropInternal(wallpaper); + return; } + int newHeight = (int) (wpData.mHeight / hRatio); + int newWidth = (int) (wpData.mWidth / hRatio); + estimateCrop.set(cropHint); estimateCrop.left += (cropHint.width() - newWidth) / 2; estimateCrop.top += (cropHint.height() - newHeight) / 2; @@ -210,8 +555,8 @@ class WallpaperCropper { // We've got the safe cropHint; now we want to scale it properly to // the desired rectangle. // That's a height-biased operation: make it fit the hinted height. - final int safeHeight = (int) (estimateCrop.height() * hRatio); - final int safeWidth = (int) (estimateCrop.width() * hRatio); + final int safeHeight = (int) (estimateCrop.height() * hRatio + 0.5f); + final int safeWidth = (int) (estimateCrop.width() * hRatio + 0.5f); if (DEBUG_CROP) { Slog.v(TAG, "Decode parameters:"); @@ -248,6 +593,12 @@ class WallpaperCropper { // We are safe to create final crop with safe dimensions now. final Bitmap finalCrop = Bitmap.createScaledBitmap(cropped, safeWidth, safeHeight, true); + + if (multiCrop) { + wallpaper.mSampleSize = + ((float) cropHint.height()) / finalCrop.getHeight(); + } + if (DEBUG) { Slog.v(TAG, "Final extract:"); Slog.v(TAG, " dims: w=" + wpData.mWidth diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index 5c867017f4e0..02594d2d8d22 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -17,6 +17,7 @@ package com.android.server.wallpaper; import static android.app.WallpaperManager.FLAG_LOCK; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP; @@ -26,6 +27,7 @@ import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; import android.app.IWallpaperManagerCallback; import android.app.WallpaperColors; +import android.app.WallpaperManager.ScreenOrientation; import android.app.WallpaperManager.SetWallpaperFlags; import android.content.ComponentName; import android.graphics.Rect; @@ -126,10 +128,16 @@ class WallpaperData { RemoteCallbackList<IWallpaperManagerCallback> callbacks = new RemoteCallbackList<>(); /** - * The crop hint supplied for displaying a subset of the source image + * Defines which part of the {@link #getWallpaperFile()} image is in the {@link #getCropFile()}. */ final Rect cropHint = new Rect(0, 0, 0, 0); + /** + * How much the crop is sub-sampled. A value > 1 means that the image quality was reduced. + * This is the ratio between the cropHint height and the actual {@link #getCropFile()} height. + */ + float mSampleSize = 1f; + // Describes the context of a call to WallpaperManagerService#bindWallpaperComponentLocked enum BindSource { UNKNOWN, @@ -156,6 +164,23 @@ class WallpaperData { private final SparseArray<File> mWallpaperFiles = new SparseArray<>(); private final SparseArray<File> mCropFiles = new SparseArray<>(); + /** + * Mapping of {@link ScreenOrientation} -> crop hint. The crop hints are relative to the + * original image stored in {@link #getWallpaperFile()}. + * Only used when multi crop flag is enabled. + */ + SparseArray<Rect> mCropHints = new SparseArray<>(); + + /** + * cropHints will be ignored if this flag is false + */ + boolean mSupportsMultiCrop; + + /** + * The phone orientation when the wallpaper was set. Only relevant for image wallpapers + */ + int mOrientationWhenSet = ORIENTATION_UNKNOWN; + WallpaperData(int userId, @SetWallpaperFlags int wallpaperType) { this.userId = userId; this.mWhich = wallpaperType; @@ -176,6 +201,10 @@ class WallpaperData { this.mWhich = source.mWhich; this.wallpaperId = source.wallpaperId; this.cropHint.set(source.cropHint); + if (source.mCropHints != null) { + this.mCropHints = source.mCropHints.clone(); + } + this.mSupportsMultiCrop = source.mSupportsMultiCrop; this.allowBackup = source.allowBackup; this.primaryColors = source.primaryColors; this.mWallpaperDimAmount = source.mWallpaperDimAmount; diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index de98df55c3ea..88e9672cd0a1 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -18,6 +18,7 @@ package com.android.server.wallpaper; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.wallpaper.WallpaperDisplayHelper.DisplayData; @@ -26,9 +27,11 @@ import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_INFO; import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked; +import static com.android.window.flags.Flags.multiCrop; import android.annotation.Nullable; import android.app.WallpaperColors; +import android.app.WallpaperManager; import android.app.WallpaperManager.SetWallpaperFlags; import android.app.backup.WallpaperBackupHelper; import android.content.ComponentName; @@ -36,7 +39,9 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Rect; import android.os.FileUtils; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; @@ -60,14 +65,16 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; +import java.util.List; import java.util.Map; /** * Helper for the wallpaper loading / saving / xml parsing * Only meant to be used lock held by WallpaperManagerService * Only meant to be instantiated once by WallpaperManagerService + * @hide */ -class WallpaperDataParser { +public class WallpaperDataParser { private static final String TAG = WallpaperDataParser.class.getSimpleName(); private static final boolean DEBUG = false; @@ -132,6 +139,7 @@ class WallpaperDataParser { */ public WallpaperLoadingResult loadSettingsLocked(int userId, boolean keepDimensionHints, boolean migrateFromOld, @SetWallpaperFlags int which) { + // TODO(b/270726737) remove the "keepDimensionHints" arg when removing the multi crop flag JournaledFile journal = makeJournaledFile(userId); FileInputStream stream = null; File file = journal.chooseForRead(); @@ -174,7 +182,9 @@ class WallpaperDataParser { WallpaperData wallpaperToParse = "wp".equals(tag) ? wallpaper : lockWallpaper; - parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints); + if (!multiCrop()) { + parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints); + } String comp = parser.getAttributeValue(null, "component"); wallpaperToParse.nextWallpaperComponent = comp != null @@ -186,6 +196,10 @@ class WallpaperDataParser { wallpaperToParse.nextWallpaperComponent = mImageWallpaper; } + if (multiCrop()) { + parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints); + } + if (DEBUG) { Slog.v(TAG, "mWidth:" + wpdData.mWidth); Slog.v(TAG, "mHeight:" + wpdData.mHeight); @@ -300,20 +314,48 @@ class WallpaperDataParser { wallpaper.wallpaperId = makeWallpaperIdLocked(); } - final DisplayData wpData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); - - if (!keepDimensionHints) { - wpData.mWidth = parser.getAttributeInt(null, "width"); - wpData.mHeight = parser.getAttributeInt(null, "height"); + Rect totalCropHint = new Rect( + getAttributeInt(parser, "totalCropLeft", 0), + getAttributeInt(parser, "totalCropTop", 0), + getAttributeInt(parser, "totalCropRight", 0), + getAttributeInt(parser, "totalCropBottom", 0)); + wallpaper.mSupportsMultiCrop = multiCrop() && ( + parser.getAttributeBoolean(null, "supportsMultiCrop", false) + || mImageWallpaper.equals(wallpaper.wallpaperComponent)); + if (wallpaper.mSupportsMultiCrop) { + wallpaper.mCropHints = new SparseArray<>(); + for (Pair<Integer, String> pair: screenDimensionPairs()) { + Rect cropHint = new Rect( + parser.getAttributeInt(null, "cropLeft" + pair.second, 0), + parser.getAttributeInt(null, "cropTop" + pair.second, 0), + parser.getAttributeInt(null, "cropRight" + pair.second, 0), + parser.getAttributeInt(null, "cropBottom" + pair.second, 0)); + if (!cropHint.isEmpty()) wallpaper.mCropHints.put(pair.first, cropHint); + } + if (wallpaper.mCropHints.size() == 0) { + // migration case: the crops per screen orientation are not specified. + // use the old attributes to find the crop for one screen orientation. + Integer orientation = totalCropHint.width() < totalCropHint.height() + ? WallpaperManager.PORTRAIT : WallpaperManager.LANDSCAPE; + if (!totalCropHint.isEmpty()) wallpaper.mCropHints.put(orientation, totalCropHint); + } else { + wallpaper.cropHint.set(totalCropHint); + } + } else { + wallpaper.cropHint.set(totalCropHint); + } + final DisplayData wpData = mWallpaperDisplayHelper + .getDisplayDataOrCreate(DEFAULT_DISPLAY); + if (!keepDimensionHints && !multiCrop()) { + wpData.mWidth = parser.getAttributeInt(null, "width", 0); + wpData.mHeight = parser.getAttributeInt(null, "height", 0); + } + if (!multiCrop()) { + wpData.mPadding.left = getAttributeInt(parser, "paddingLeft", 0); + wpData.mPadding.top = getAttributeInt(parser, "paddingTop", 0); + wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0); + wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0); } - wallpaper.cropHint.left = getAttributeInt(parser, "cropLeft", 0); - wallpaper.cropHint.top = getAttributeInt(parser, "cropTop", 0); - wallpaper.cropHint.right = getAttributeInt(parser, "cropRight", 0); - wallpaper.cropHint.bottom = getAttributeInt(parser, "cropBottom", 0); - wpData.mPadding.left = getAttributeInt(parser, "paddingLeft", 0); - wpData.mPadding.top = getAttributeInt(parser, "paddingTop", 0); - wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0); - wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0); wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f); BindSource bindSource; try { @@ -365,11 +407,11 @@ class WallpaperDataParser { wallpaper.allowBackup = parser.getAttributeBoolean(null, "backup", false); } - private int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) { + private static int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) { return parser.getAttributeInt(null, name, defValue); } - private float getAttributeFloat(TypedXmlPullParser parser, String name, float defValue) { + private static float getAttributeFloat(TypedXmlPullParser parser, String name, float defValue) { return parser.getAttributeFloat(null, name, defValue); } @@ -412,28 +454,66 @@ class WallpaperDataParser { if (DEBUG) { Slog.v(TAG, "writeWallpaperAttributes id=" + wallpaper.wallpaperId); } - final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); out.startTag(null, tag); out.attributeInt(null, "id", wallpaper.wallpaperId); - out.attributeInt(null, "width", wpdData.mWidth); - out.attributeInt(null, "height", wpdData.mHeight); - out.attributeInt(null, "cropLeft", wallpaper.cropHint.left); - out.attributeInt(null, "cropTop", wallpaper.cropHint.top); - out.attributeInt(null, "cropRight", wallpaper.cropHint.right); - out.attributeInt(null, "cropBottom", wallpaper.cropHint.bottom); + out.attributeBoolean(null, "supportsMultiCrop", wallpaper.mSupportsMultiCrop); - if (wpdData.mPadding.left != 0) { - out.attributeInt(null, "paddingLeft", wpdData.mPadding.left); - } - if (wpdData.mPadding.top != 0) { - out.attributeInt(null, "paddingTop", wpdData.mPadding.top); - } - if (wpdData.mPadding.right != 0) { - out.attributeInt(null, "paddingRight", wpdData.mPadding.right); - } - if (wpdData.mPadding.bottom != 0) { - out.attributeInt(null, "paddingBottom", wpdData.mPadding.bottom); + if (multiCrop() && wallpaper.mSupportsMultiCrop) { + if (wallpaper.mCropHints == null) { + Slog.e(TAG, "cropHints should not be null when saved"); + wallpaper.mCropHints = new SparseArray<>(); + } + for (Pair<Integer, String> pair : screenDimensionPairs()) { + Rect cropHint = wallpaper.mCropHints.get(pair.first); + if (cropHint == null) continue; + out.attributeInt(null, "cropLeft" + pair.second, cropHint.left); + out.attributeInt(null, "cropTop" + pair.second, cropHint.top); + out.attributeInt(null, "cropRight" + pair.second, cropHint.right); + out.attributeInt(null, "cropBottom" + pair.second, cropHint.bottom); + + // to support back compatibility in B&R, save the crops for one orientation in the + // legacy "cropLeft", "cropTop", "cropRight", "cropBottom" entries + int orientationToPutInLegacyCrop = wallpaper.mOrientationWhenSet; + if (mWallpaperDisplayHelper.isFoldable()) { + int unfoldedOrientation = mWallpaperDisplayHelper + .getUnfoldedOrientation(orientationToPutInLegacyCrop); + if (unfoldedOrientation != ORIENTATION_UNKNOWN) { + orientationToPutInLegacyCrop = unfoldedOrientation; + } + } + if (pair.first == orientationToPutInLegacyCrop) { + out.attributeInt(null, "cropLeft", cropHint.left); + out.attributeInt(null, "cropTop", cropHint.top); + out.attributeInt(null, "cropRight", cropHint.right); + out.attributeInt(null, "cropBottom", cropHint.bottom); + } + } + out.attributeInt(null, "totalCropLeft", wallpaper.cropHint.left); + out.attributeInt(null, "totalCropTop", wallpaper.cropHint.top); + out.attributeInt(null, "totalCropRight", wallpaper.cropHint.right); + out.attributeInt(null, "totalCropBottom", wallpaper.cropHint.bottom); + } else if (!multiCrop()) { + final DisplayData wpdData = + mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); + out.attributeInt(null, "width", wpdData.mWidth); + out.attributeInt(null, "height", wpdData.mHeight); + out.attributeInt(null, "cropLeft", wallpaper.cropHint.left); + out.attributeInt(null, "cropTop", wallpaper.cropHint.top); + out.attributeInt(null, "cropRight", wallpaper.cropHint.right); + out.attributeInt(null, "cropBottom", wallpaper.cropHint.bottom); + if (wpdData.mPadding.left != 0) { + out.attributeInt(null, "paddingLeft", wpdData.mPadding.left); + } + if (wpdData.mPadding.top != 0) { + out.attributeInt(null, "paddingTop", wpdData.mPadding.top); + } + if (wpdData.mPadding.right != 0) { + out.attributeInt(null, "paddingRight", wpdData.mPadding.right); + } + if (wpdData.mPadding.bottom != 0) { + out.attributeInt(null, "paddingBottom", wpdData.mPadding.bottom); + } } out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount); @@ -564,4 +644,12 @@ class WallpaperDataParser { } return false; } + + private static List<Pair<Integer, String>> screenDimensionPairs() { + return List.of( + new Pair<>(WallpaperManager.PORTRAIT, "Portrait"), + new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"), + new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"), + new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape")); + } } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java index f48178c5b9f7..19fd9a90518d 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java @@ -16,19 +16,31 @@ package com.android.server.wallpaper; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; +import static android.app.WallpaperManager.getRotatedOrientation; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.window.flags.Flags.multiCrop; + +import android.app.WallpaperManager; +import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Debug; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.WindowManager; +import android.view.WindowMetrics; import com.android.server.wm.WindowManagerInternal; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import java.util.function.Consumer; /** @@ -50,12 +62,55 @@ class WallpaperDisplayHelper { private final SparseArray<DisplayData> mDisplayDatas = new SparseArray<>(); private final DisplayManager mDisplayManager; private final WindowManagerInternal mWindowManagerInternal; + private final SparseArray<Point> mDefaultDisplaySizes = new SparseArray<>(); + + // related orientations pairs for foldable (folded orientation, unfolded orientation) + private final List<Pair<Integer, Integer>> mFoldableOrientationPairs = new ArrayList<>(); + + private boolean mIsFoldable; WallpaperDisplayHelper( DisplayManager displayManager, - WindowManagerInternal windowManagerInternal) { + WindowManager windowManager, + WindowManagerInternal windowManagerInternal, + boolean isFoldable) { mDisplayManager = displayManager; mWindowManagerInternal = windowManagerInternal; + mIsFoldable = isFoldable; + if (!multiCrop()) return; + Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); + boolean populateOrientationPairs = isFoldable && metrics.size() == 2; + float surface = 0; + int firstOrientation = -1; + for (WindowMetrics metric: metrics) { + Rect bounds = metric.getBounds(); + Point displaySize = new Point(bounds.width(), bounds.height()); + Point reversedDisplaySize = new Point(displaySize.y, displaySize.x); + for (Point point : List.of(displaySize, reversedDisplaySize)) { + int orientation = WallpaperManager.getOrientation(point); + // don't add an entry if there is already a larger display of the same orientation + Point display = mDefaultDisplaySizes.get(orientation); + if (display == null || display.x * display.y < point.x * point.y) { + mDefaultDisplaySizes.put(orientation, point); + } + } + if (populateOrientationPairs) { + int orientation = WallpaperManager.getOrientation(displaySize); + float newSurface = displaySize.x * displaySize.y * metric.getDensity(); + if (surface <= 0) { + surface = newSurface; + firstOrientation = orientation; + } else { + Pair<Integer, Integer> pair = (newSurface > surface) + ? new Pair<>(firstOrientation, orientation) + : new Pair<>(orientation, firstOrientation); + Pair<Integer, Integer> rotatedPair = new Pair<>( + getRotatedOrientation(pair.first), getRotatedOrientation(pair.second)); + mFoldableOrientationPairs.add(pair); + mFoldableOrientationPairs.add(rotatedPair); + } + } + } } DisplayData getDisplayDataOrCreate(int displayId) { @@ -68,6 +123,12 @@ class WallpaperDisplayHelper { return wpdData; } + int getDefaultDisplayCurrentOrientation() { + Point displaySize = new Point(); + mDisplayManager.getDisplay(DEFAULT_DISPLAY).getSize(displaySize); + return WallpaperManager.getOrientation(displaySize); + } + void removeDisplayData(int displayId) { mDisplayDatas.remove(displayId); } @@ -133,4 +194,46 @@ class WallpaperDisplayHelper { boolean isValidDisplay(int displayId) { return mDisplayManager.getDisplay(displayId) != null; } + + SparseArray<Point> getDefaultDisplaySizes() { + return mDefaultDisplaySizes; + } + + /** Return the number of pixel of the largest dimension of the default display */ + int getDefaultDisplayLargestDimension() { + int result = -1; + for (int i = 0; i < mDefaultDisplaySizes.size(); i++) { + Point size = mDefaultDisplaySizes.valueAt(i); + result = Math.max(result, Math.max(size.x, size.y)); + } + return result; + } + + boolean isFoldable() { + return mIsFoldable; + } + + /** + * If a given orientation corresponds to an unfolded orientation on foldable, return the + * corresponding folded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the + * device is not a foldable. + */ + int getFoldedOrientation(int orientation) { + for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) { + if (pair.second.equals(orientation)) return pair.first; + } + return ORIENTATION_UNKNOWN; + } + + /** + * If a given orientation corresponds to a folded orientation on foldable, return the + * corresponding unfolded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the + * device is not a foldable. + */ + int getUnfoldedOrientation(int orientation) { + for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) { + if (pair.first.equals(orientation)) return pair.second; + } + return ORIENTATION_UNKNOWN; + } } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 3782b429f93a..cd998a2fdf0e 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -22,6 +22,7 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREG import static android.app.WallpaperManager.COMMAND_REAPPLY; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.ParcelFileDescriptor.MODE_CREATE; @@ -74,6 +75,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.hardware.display.DisplayManager; @@ -103,12 +105,15 @@ import android.service.wallpaper.IWallpaperService; import android.service.wallpaper.WallpaperService; import android.system.ErrnoException; import android.system.Os; +import android.text.TextUtils; import android.util.EventLog; import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.Display; +import android.view.View; +import android.view.WindowManager; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; @@ -137,6 +142,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -189,8 +195,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } private final Object mLock = new Object(); - /** True to support different crops for different display dimensions */ - private final boolean mIsMultiCropEnabled; /** Tracks wallpaper being migrated from system+lock to lock when setting static wp. */ WallpaperDestinationChangeHandler mPendingMigrationViaStatic; @@ -804,6 +808,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub null /* options */); mWindowManagerInternal.setWallpaperShowWhenLocked( mToken, (wallpaper.mWhich & FLAG_LOCK) != 0); + if (multiCrop() && wallpaper.mSupportsMultiCrop) { + mWindowManagerInternal.setWallpaperCropHints(mToken, + mWallpaperCropper.getRelativeCropHints(wallpaper)); + } else { + mWindowManagerInternal.setWallpaperCropHints(mToken, new SparseArray<>()); + } final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(mDisplayId); try { @@ -1479,10 +1489,15 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mIPackageManager = AppGlobals.getPackageManager(); mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); - DisplayManager dm = mContext.getSystemService(DisplayManager.class); - dm.registerDisplayListener(mDisplayListener, null /* handler */); - mWallpaperDisplayHelper = new WallpaperDisplayHelper(dm, mWindowManagerInternal); + DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); + displayManager.registerDisplayListener(mDisplayListener, null /* handler */); + WindowManager windowManager = mContext.getSystemService(WindowManager.class); + boolean isFoldable = mContext.getResources() + .getIntArray(R.array.config_foldedDeviceStates).length > 0; + mWallpaperDisplayHelper = new WallpaperDisplayHelper( + displayManager, windowManager, mWindowManagerInternal, isFoldable); mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper); + mWindowManagerInternal.setWallpaperCropUtils(mWallpaperCropper::getCrop); mActivityManager = mContext.getSystemService(ActivityManager.class); if (mContext.getResources().getBoolean( @@ -1522,7 +1537,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mColorsChangedListeners = new SparseArray<>(); mWallpaperDataParser = new WallpaperDataParser(mContext, mWallpaperDisplayHelper, mWallpaperCropper); - mIsMultiCropEnabled = multiCrop(); LocalServices.addService(WallpaperManagerInternal.class, new LocalService()); } @@ -2199,6 +2213,66 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } + @Override + public List<Rect> getBitmapCrops(List<Point> displaySizes, @SetWallpaperFlags int which, + boolean originalBitmap, int userId) { + userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), + Binder.getCallingUid(), userId, false, true, "getBitmapCrop", null); + synchronized (mLock) { + checkPermission(READ_WALLPAPER_INTERNAL); + WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId) + : mWallpaperMap.get(userId); + if (wallpaper == null || !wallpaper.mSupportsMultiCrop) return null; + SparseArray<Rect> relativeSuggestedCrops = + mWallpaperCropper.getRelativeCropHints(wallpaper); + Point croppedBitmapSize = + new Point(wallpaper.cropHint.width(), wallpaper.cropHint.height()); + SparseArray<Rect> relativeDefaultCrops = + mWallpaperCropper.getDefaultCrops(relativeSuggestedCrops, croppedBitmapSize); + SparseArray<Rect> adjustedRelativeSuggestedCrops = new SparseArray<>(); + for (int i = 0; i < relativeDefaultCrops.size(); i++) { + int key = relativeDefaultCrops.keyAt(i); + if (relativeSuggestedCrops.contains(key)) { + adjustedRelativeSuggestedCrops.put(key, relativeDefaultCrops.get(key)); + } + } + List<Rect> result = new ArrayList<>(); + boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + for (Point displaySize : displaySizes) { + result.add(mWallpaperCropper.getCrop( + displaySize, croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl)); + } + if (originalBitmap) result = WallpaperCropper.getOriginalCropHints(wallpaper, result); + return result; + } + } + + @Override + public List<Rect> getFutureBitmapCrops(Point bitmapSize, List<Point> displaySizes, + int[] screenOrientations, List<Rect> crops) { + SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops, ORIENTATION_UNKNOWN); + SparseArray<Rect> defaultCrops = mWallpaperCropper.getDefaultCrops(cropMap, bitmapSize); + List<Rect> result = new ArrayList<>(); + boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + for (Point displaySize : displaySizes) { + result.add(mWallpaperCropper.getCrop(displaySize, bitmapSize, defaultCrops, rtl)); + } + return result; + } + + @Override + public Rect getBitmapCrop(Point bitmapSize, int[] screenOrientations, List<Rect> crops) { + if (!multiCrop()) { + throw new UnsupportedOperationException( + "This method should only be called with the multi crop flag enabled"); + } + SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops, ORIENTATION_UNKNOWN); + SparseArray<Rect> defaultCrops = mWallpaperCropper.getDefaultCrops(cropMap, bitmapSize); + return WallpaperCropper.getTotalCrop(defaultCrops); + } + private boolean hasPermission(String permission) { return mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED; } @@ -2755,8 +2829,18 @@ public class WallpaperManagerService extends IWallpaperManager.Stub @Override public ParcelFileDescriptor setWallpaper(String name, String callingPackage, - Rect cropHint, boolean allowBackup, Bundle extras, int which, - IWallpaperManagerCallback completion, int userId) { + int[] screenOrientations, List<Rect> crops, boolean allowBackup, + Bundle extras, int which, IWallpaperManagerCallback completion, int userId) { + + if (DEBUG) { + Slog.d(TAG, "setWallpaper: name = " + name + ", callingPackage = " + callingPackage + + ", screenOrientations = " + + (screenOrientations == null ? null + : Arrays.stream(screenOrientations).boxed().toList()) + + ", crops = " + crops + + ", allowBackup = " + allowBackup); + } + userId = ActivityManager.handleIncomingUser(getCallingPid(), getCallingUid(), userId, false /* all */, true /* full */, "changing wallpaper", null /* pkg */); checkPermission(android.Manifest.permission.SET_WALLPAPER); @@ -2771,10 +2855,17 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return null; } + int currentOrientation = mWallpaperDisplayHelper.getDefaultDisplayCurrentOrientation(); + SparseArray<Rect> cropMap = !multiCrop() ? null + : getCropMap(screenOrientations, crops, currentOrientation); + Rect cropHint = multiCrop() || crops == null ? null : crops.get(0); + final boolean fromForegroundApp = !multiCrop() ? false + : isFromForegroundApp(callingPackage); + // "null" means the no-op crop, preserving the full input image - if (cropHint == null) { + if (cropHint == null && !multiCrop()) { cropHint = new Rect(0, 0, 0, 0); - } else { + } else if (!multiCrop()) { if (cropHint.width() < 0 || cropHint.height() < 0 || cropHint.left < 0 || cropHint.top < 0) { @@ -2814,10 +2905,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub wallpaper.mSystemWasBoth = systemIsBoth; wallpaper.mWhich = which; wallpaper.setComplete = completion; - wallpaper.fromForegroundApp = isFromForegroundApp(callingPackage); - wallpaper.cropHint.set(cropHint); + wallpaper.fromForegroundApp = multiCrop() ? fromForegroundApp + : isFromForegroundApp(callingPackage); + if (!multiCrop()) wallpaper.cropHint.set(cropHint); + if (multiCrop()) wallpaper.mSupportsMultiCrop = true; + if (multiCrop()) wallpaper.mCropHints = cropMap; wallpaper.allowBackup = allowBackup; wallpaper.mWallpaperDimAmount = getWallpaperDimAmount(); + wallpaper.mOrientationWhenSet = currentOrientation; } return pfd; } finally { @@ -2826,11 +2921,47 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } + private SparseArray<Rect> getCropMap(int[] screenOrientations, List<Rect> crops, + int currentOrientation) { + if ((crops == null ^ screenOrientations == null) + || (crops != null && crops.size() != screenOrientations.length)) { + throw new IllegalArgumentException( + "Illegal crops/orientations lists: must both be null, or both the same size"); + } + SparseArray<Rect> cropMap = new SparseArray<>(); + boolean unknown = false; + if (crops != null && crops.size() != 0) { + for (int i = 0; i < crops.size(); i++) { + Rect crop = crops.get(i); + int width = crop.width(), height = crop.height(); + if (width < 0 || height < 0 || crop.left < 0 || crop.top < 0) { + throw new IllegalArgumentException("Invalid crop rect supplied: " + crop); + } + int orientation = screenOrientations[i]; + if (orientation == ORIENTATION_UNKNOWN) { + if (currentOrientation == ORIENTATION_UNKNOWN) { + throw new IllegalArgumentException( + "Invalid orientation: " + ORIENTATION_UNKNOWN); + } + unknown = true; + orientation = currentOrientation; + } + cropMap.put(orientation, crop); + } + } + if (unknown && cropMap.size() > 1) { + throw new IllegalArgumentException("Invalid crops supplied: the UNKNOWN screen " + + "orientation should only be used in a singleton map (in which case it" + + "represents the current orientation of the default display)"); + } + return cropMap; + } + private void migrateStaticSystemToLockWallpaperLocked(int userId) { WallpaperData sysWP = mWallpaperMap.get(userId); if (sysWP == null) { if (DEBUG) { - Slog.i(TAG, "No system wallpaper? Not tracking for lock-only"); + Slog.i(TAG, "No system wallpaper? Not tracking for lock-only"); } return; } @@ -2839,6 +2970,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub WallpaperData lockWP = new WallpaperData(userId, FLAG_LOCK); lockWP.wallpaperId = sysWP.wallpaperId; lockWP.cropHint.set(sysWP.cropHint); + lockWP.mSupportsMultiCrop = sysWP.mSupportsMultiCrop; + if (sysWP.mCropHints != null) { + lockWP.mCropHints = sysWP.mCropHints.clone(); + } lockWP.allowBackup = sysWP.allowBackup; lockWP.primaryColors = sysWP.primaryColors; lockWP.mWallpaperDimAmount = sysWP.mWallpaperDimAmount; @@ -2956,6 +3091,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final long ident = Binder.clearCallingIdentity(); try { + newWallpaper.mSupportsMultiCrop = mImageWallpaper.equals(name); newWallpaper.imageWallpaperPending = false; newWallpaper.mWhich = which; newWallpaper.mSystemWasBoth = systemIsBoth; @@ -3428,11 +3564,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return (wallpaper != null) ? wallpaper.allowBackup : false; } - @Override - public boolean isMultiCropEnabled() { - return mIsMultiCropEnabled; - } - private void onDisplayReadyInternal(int displayId) { synchronized (mLock) { if (mLastWallpaper == null) { diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index a9f0554b2bec..d68f932400a2 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -34,6 +34,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.H.WALLPAPER_DRAW_PENDING_TIMEOUT; +import static com.android.window.flags.Flags.multiCrop; import android.annotation.Nullable; import android.content.res.Resources; @@ -48,6 +49,7 @@ import android.os.SystemClock; import android.util.ArraySet; import android.util.MathUtils; import android.util.Slog; +import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; import android.view.SurfaceControl; @@ -60,6 +62,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLogImpl; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ToBooleanFunction; +import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import java.io.PrintWriter; import java.util.ArrayList; @@ -73,6 +76,7 @@ import java.util.function.Consumer; class WallpaperController { private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM; private WindowManagerService mService; + private WallpaperCropUtils mWallpaperCropUtils = null; private DisplayContent mDisplayContent; private final ArrayList<WallpaperWindowToken> mWallpaperTokens = new ArrayList<>(); @@ -240,9 +244,8 @@ class WallpaperController { mMinWallpaperScale = resources.getFloat(com.android.internal.R.dimen.config_wallpaperMinScale); mMaxWallpaperScale = resources.getFloat(R.dimen.config_wallpaperMaxScale); - mShouldOffsetWallpaperCenter = - resources.getBoolean( - com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay); + mShouldOffsetWallpaperCenter = resources.getBoolean( + com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay); } void resetLargestDisplay(Display display) { @@ -266,7 +269,7 @@ class WallpaperController { } @Nullable private Point findLargestDisplaySize() { - if (!mShouldOffsetWallpaperCenter) { + if (!mShouldOffsetWallpaperCenter || multiCrop()) { return null; } Point largestDisplaySize = new Point(); @@ -284,6 +287,10 @@ class WallpaperController { return largestDisplaySize; } + void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) { + mWallpaperCropUtils = wallpaperCropUtils; + } + WindowState getWallpaperTarget() { return mWallpaperTarget; } @@ -357,26 +364,92 @@ class WallpaperController { boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) { // Size of the display the wallpaper is rendered on. final Rect lastWallpaperBounds = wallpaperWin.getParentFrame(); - // Full size of the wallpaper (usually larger than bounds above to parallax scroll when - // swiping through Launcher pages). - final Rect wallpaperFrame = wallpaperWin.getFrame(); + int screenWidth = lastWallpaperBounds.width(); + int screenHeight = lastWallpaperBounds.height(); + float screenRatio = ((float) screenWidth) / screenHeight; + Point screenSize = new Point(screenWidth, screenHeight); + WallpaperWindowToken token = wallpaperWin.mToken.asWallpaperToken(); - final int diffWidth = wallpaperFrame.width() - lastWallpaperBounds.width(); - final int diffHeight = wallpaperFrame.height() - lastWallpaperBounds.height(); - if ((wallpaperWin.mAttrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0 - && Math.abs(diffWidth) > 1 && Math.abs(diffHeight) > 1) { - Slog.d(TAG, "Skip wallpaper offset with inconsistent orientation, bounds=" - + lastWallpaperBounds + " frame=" + wallpaperFrame); - // With FLAG_SCALED, the requested size should at least make the frame match one of - // side. If both sides contain differences, the client side may not have updated the - // latest size according to the current orientation. So skip calculating the offset to - // avoid the wallpaper not filling the screen. - return false; + /* + * TODO(b/270726737) adapt comments once flag gets removed and multiCrop is always true + * Size of the wallpaper. May have more width/height ratio than the screen for parallax. + * + * If multiCrop is true, we use a map, cropHints, defining which sub-area of the wallpaper + * to show for a given screen orientation. In this case, wallpaperFrame represents the + * sub-area of WallpaperWin to show for the current screen size. + * + * If multiCrop is false, don't show a custom sub-area of the wallpaper. Just show the + * whole wallpaperWin if possible, and center and zoom if necessary. + */ + final Rect wallpaperFrame; + + /* + * The values cropZoom, cropOffsetX and cropOffsetY are only used if multiCrop is true. + * Zoom and offsets to be applied in order to show wallpaperFrame on screen. + */ + final float cropZoom; + final int cropOffsetX; + final int cropOffsetY; + + /* + * Difference of width/height between the wallpaper and the screen. + * This is the additional room that we have to apply offsets (i.e. parallax). + */ + final int diffWidth; + final int diffHeight; + + /* + * zoom, offsetX and offsetY are not related to cropping the wallpaper: + * - zoom is used to apply an additional zoom (e.g. for launcher animations). + * - offsetX, offsetY are used to apply an offset to the wallpaper (e.g. parallax effect). + */ + final float zoom; + int offsetX; + int offsetY; + + if (multiCrop()) { + if (mWallpaperCropUtils == null) { + Slog.e(TAG, "Update wallpaper offsets before the system is ready. Aborting"); + return false; + } + Point bitmapSize = new Point( + wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight); + SparseArray<Rect> cropHints = token.getCropHints(); + wallpaperFrame = mWallpaperCropUtils.getCrop( + screenSize, bitmapSize, cropHints, wallpaperWin.isRtl()); + + cropZoom = wallpaperFrame.isEmpty() ? 1f + : ((float) screenHeight) / wallpaperFrame.height() / wallpaperWin.mVScale; + + // A positive x / y offset shifts the wallpaper to the right / bottom respectively. + cropOffsetX = -wallpaperFrame.left + + (int) ((cropZoom - 1f) * wallpaperFrame.height() * screenRatio / 2f); + cropOffsetY = -wallpaperFrame.top + + (int) ((cropZoom - 1f) * wallpaperFrame.height() / 2f); + + diffWidth = (int) (wallpaperFrame.width() * wallpaperWin.mHScale) - screenWidth; + diffHeight = (int) (wallpaperFrame.height() * wallpaperWin.mVScale) - screenHeight; + } else { + wallpaperFrame = wallpaperWin.getFrame(); + cropZoom = 1f; + cropOffsetX = 0; + cropOffsetY = 0; + diffWidth = wallpaperFrame.width() - screenWidth; + diffHeight = wallpaperFrame.height() - screenHeight; + + if ((wallpaperWin.mAttrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0 + && Math.abs(diffWidth) > 1 && Math.abs(diffHeight) > 1) { + Slog.d(TAG, "Skip wallpaper offset with inconsistent orientation, bounds=" + + lastWallpaperBounds + " frame=" + wallpaperFrame); + // With FLAG_SCALED, the requested size should at least make the frame match one of + // side. If both sides contain differences, the client side may not have updated the + // latest size according to the current orientation. So skip calculating the offset + // to avoid the wallpaper not filling the screen. + return false; + } } - int newXOffset = 0; - int newYOffset = 0; boolean rawChanged = false; // Set the default wallpaper x-offset to either edge of the screen (depending on RTL), to // match the behavior of most Launchers @@ -396,17 +469,17 @@ class WallpaperController { int displayOffset = getDisplayWidthOffset(availw, lastWallpaperBounds, wallpaperWin.isRtl()); availw -= displayOffset; - int offset = availw > 0 ? -(int)(availw * wpx + .5f) : 0; + offsetX = availw > 0 ? -(int) (availw * wpx + .5f) : 0; if (token.mWallpaperDisplayOffsetX != Integer.MIN_VALUE) { // if device is LTR, then offset wallpaper to the left (the wallpaper is drawn // always starting from the left of the screen). - offset += token.mWallpaperDisplayOffsetX; + offsetX += token.mWallpaperDisplayOffsetX; } else if (!wallpaperWin.isRtl()) { // In RTL the offset is calculated so that the wallpaper ends up right aligned (see // offset above). - offset -= displayOffset; + offsetX -= displayOffset; } - newXOffset = offset; + offsetX += cropOffsetX * wallpaperWin.mHScale; if (wallpaperWin.mWallpaperX != wpx || wallpaperWin.mWallpaperXStep != wpxs) { wallpaperWin.mWallpaperX = wpx; @@ -416,11 +489,11 @@ class WallpaperController { float wpy = token.mWallpaperY >= 0 ? token.mWallpaperY : 0.5f; float wpys = token.mWallpaperYStep >= 0 ? token.mWallpaperYStep : -1.0f; - offset = diffHeight > 0 ? -(int) (diffHeight * wpy + .5f) : 0; + offsetY = diffHeight > 0 ? -(int) (diffHeight * wpy + .5f) : 0; if (token.mWallpaperDisplayOffsetY != Integer.MIN_VALUE) { - offset += token.mWallpaperDisplayOffsetY; + offsetY += token.mWallpaperDisplayOffsetY; } - newYOffset = offset; + offsetY += cropOffsetY * wallpaperWin.mVScale; if (wallpaperWin.mWallpaperY != wpy || wallpaperWin.mWallpaperYStep != wpys) { wallpaperWin.mWallpaperY = wpy; @@ -432,10 +505,10 @@ class WallpaperController { wallpaperWin.mWallpaperZoomOut = mLastWallpaperZoomOut; rawChanged = true; } - - boolean changed = wallpaperWin.setWallpaperOffset(newXOffset, newYOffset, - wallpaperWin.mShouldScaleWallpaper - ? zoomOutToScale(wallpaperWin.mWallpaperZoomOut) : 1); + zoom = wallpaperWin.mShouldScaleWallpaper + ? zoomOutToScale(wallpaperWin.mWallpaperZoomOut) : 1f; + final float totalZoom = zoom * cropZoom; + boolean changed = wallpaperWin.setWallpaperOffset(offsetX, offsetY, totalZoom); if (rawChanged && (wallpaperWin.mAttrs.privateFlags & WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS) != 0) { @@ -496,7 +569,7 @@ class WallpaperController { * display). */ private int getDisplayWidthOffset(int availWidth, Rect displayFrame, boolean isRtl) { - if (!mShouldOffsetWallpaperCenter) { + if (!mShouldOffsetWallpaperCenter || multiCrop()) { return 0; } if (mLargestDisplaySize == null) { diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 15bd6078dc2d..1bcd882b5d64 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -25,9 +25,11 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.Nullable; +import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.util.SparseArray; import android.view.animation.Animation; import com.android.internal.protolog.common.ProtoLog; @@ -49,6 +51,12 @@ class WallpaperWindowToken extends WindowToken { int mWallpaperDisplayOffsetX = Integer.MIN_VALUE; int mWallpaperDisplayOffsetY = Integer.MIN_VALUE; + /** + * Map from {@link android.app.WallpaperManager.ScreenOrientation} to crop rectangles. + * Crop rectangles represent the part of the wallpaper displayed for each screen orientation. + */ + private SparseArray<Rect> mCropHints = new SparseArray<>(); + WallpaperWindowToken(WindowManagerService service, IBinder token, boolean explicit, DisplayContent dc, boolean ownerCanManageAppTokens) { this(service, token, explicit, dc, ownerCanManageAppTokens, null /* options */); @@ -98,6 +106,14 @@ class WallpaperWindowToken extends WindowToken { return mShowWhenLocked; } + void setCropHints(SparseArray<Rect> cropHints) { + mCropHints = cropHints.clone(); + } + + SparseArray<Rect> getCropHints() { + return mCropHints; + } + void sendWindowWallpaperCommand( String action, int x, int y, int z, Bundle extras, boolean sync) { for (int wallpaperNdx = mChildren.size() - 1; wallpaperNdx >= 0; wallpaperNdx--) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 22b690e85c35..b55b8a7c11bb 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -33,6 +33,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Message; import android.util.Pair; +import android.util.SparseArray; import android.view.ContentRecordingSession; import android.view.Display; import android.view.IInputFilter; @@ -51,6 +52,7 @@ import android.window.ScreenCapture; import com.android.internal.policy.KeyInterceptionInfo; import com.android.server.input.InputManagerService; import com.android.server.policy.WindowManagerPolicy; +import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.server.wm.SensitiveContentPackages.PackageInfo; import java.lang.annotation.Retention; @@ -698,6 +700,21 @@ public abstract class WindowManagerInternal { public abstract void setWallpaperShowWhenLocked(IBinder windowToken, boolean showWhenLocked); /** + * Sets the crop hints of a {@link WallpaperWindowToken}. Only effective for image wallpapers. + * + * @param windowToken wallpaper token previously added via {@link #addWindowToken} + * @param cropHints a map that represents which part of the wallpaper should be shown, for + * each type of {@link android.app.WallpaperManager.ScreenOrientation}. + */ + public abstract void setWallpaperCropHints(IBinder windowToken, SparseArray<Rect> cropHints); + + /** + * Transmits the {@link WallpaperCropUtils} instance to {@link WallpaperController}. + * {@link WallpaperCropUtils} contains the helpers to properly position the wallpaper. + */ + public abstract void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils); + + /** * Returns {@code true} if a Window owned by {@code uid} has focus. */ public abstract boolean isUidFocused(int uid); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9179acf2caed..f40b4c91ca69 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -153,6 +153,7 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.INPUT_METHOD_W import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; +import static com.android.window.flags.Flags.multiCrop; import android.Manifest; import android.Manifest.permission; @@ -238,6 +239,7 @@ import android.util.EventLog; import android.util.MergedConfiguration; import android.util.Pair; import android.util.Slog; +import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.TimeUtils; @@ -342,6 +344,7 @@ import com.android.server.policy.WindowManagerPolicy; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.power.ShutdownThread; import com.android.server.utils.PriorityDump; +import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import dalvik.annotation.optimization.NeverCompile; @@ -8129,6 +8132,25 @@ public class WindowManagerService extends IWindowManager.Stub } @Override + public void setWallpaperCropHints(IBinder binder, SparseArray<Rect> cropHints) { + synchronized (mGlobalLock) { + final WindowToken token = mRoot.getWindowToken(binder); + if (token == null || token.asWallpaperToken() == null) { + ProtoLog.w(WM_ERROR, + "setWallpaperCropHints: non-existent wallpaper token: %s", binder); + return; + } + token.asWallpaperToken().setCropHints(cropHints); + } + } + + @Override + public void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) { + mRoot.getDisplayContent(DEFAULT_DISPLAY).mWallpaperController + .setWallpaperCropUtils(wallpaperCropUtils); + } + + @Override public boolean isUidFocused(int uid) { synchronized (mGlobalLock) { for (int i = mRoot.getChildCount() - 1; i >= 0; i--) { @@ -9354,7 +9376,8 @@ public class WindowManagerService extends IWindowManager.Stub final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { - if (!mAtmService.isCallerRecents(callingUid)) { + if (!mAtmService.isCallerRecents(callingUid) + && (!multiCrop() || callingUid != SYSTEM_UID)) { Slog.e(TAG, "Unable to verify uid for getPossibleDisplayInfo" + " on uid " + callingUid); return new ArrayList<>(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index 6216acbfe465..73d386a328f5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -32,6 +32,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.window.flags.Flags.multiCrop; import static com.google.common.truth.Truth.assertThat; @@ -39,6 +40,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; @@ -484,6 +486,7 @@ public class WallpaperControllerTests extends WindowTestsBase { @Test public void testUpdateWallpaperOffset_resize_shouldCenterEnabled() { + assumeFalse(multiCrop()); final DisplayContent dc = new TestDisplayContent.Builder(mAtm, INITIAL_WIDTH, INITIAL_HEIGHT).build(); dc.mWallpaperController.setShouldOffsetWallpaperCenter(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 7ae5a1156d07..114b9c3a68f2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -119,6 +119,7 @@ import android.window.TransitionRequestInfo; import com.android.internal.policy.AttributeCache; import com.android.internal.util.ArrayUtils; import com.android.internal.util.test.FakeSettingsProvider; +import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry; import org.junit.After; @@ -286,6 +287,18 @@ class WindowTestsBase extends SystemServiceTestsBase { mAtm.mWindowManager.mLetterboxConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false); + // Setup WallpaperController crop utils with a simple center-align strategy + WallpaperCropUtils cropUtils = (displaySize, bitmapSize, suggestedCrops, rtl) -> { + Rect crop = new Rect(0, 0, displaySize.x, displaySize.y); + crop.scale(Math.min( + ((float) bitmapSize.x) / displaySize.x, + ((float) bitmapSize.y) / displaySize.y)); + crop.offset((bitmapSize.x - crop.width()) / 2, (bitmapSize.y - crop.height()) / 2); + return crop; + }; + mDisplayContent.mWallpaperController.setWallpaperCropUtils(cropUtils); + mDefaultDisplay.mWallpaperController.setWallpaperCropUtils(cropUtils); + checkDeviceSpecificOverridesNotApplied(); } |