Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d7182f30 authored by Aurélien Pomini's avatar Aurélien Pomini
Browse files

Fix B&R for multi crop

The method findNewCropFromOldCrop was not yet used in the multi-crop
case. Use it to adjust every crop. Also modify the method slightly:
 - Make it work when the target device is wider than source device
 - Prefer adding content to the crop rather than removing some, even if
   we need to add more content on one side. In most cases we'd rather
   break symmetry one one axis than removing content (i.e. zooming)

Also, drop the LANDSCAPE crop hint in restore unless the new device has
a large screen (min width > 600 dpi). We don't really care about the
landscape crop except for tablet devices, and keeping it can lead to
difficult to explain behaviours (e.g. it may affect wallpaper position
on connected displays).

Refactor the logic a bit to mock the display and bitmap sizes to make
the crop logic easily testable (Display and DisplayManager are final).
Then, add a couple test cases. There are way too many possible B&R
combinations; so pick 15 that are realistic and cover more or less all
the logic. These tests expect exact matches but are well commented.

Flag: android.app.fix_wallpaper_crops_on_restore
Test: manual
Test: atest WallpaperBackupAgentTest
Bug: 332937943

Change-Id: I224e52d4804ff7cba993e7ceeb4c9af38c0192e0
parent 724367e8
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -55,3 +55,13 @@ flag {
    purpose: PURPOSE_BUGFIX
  }
}

flag {
  name: "fix_wallpaper_crops_on_restore"
  namespace: "systemui"
  description: "Preserve center for wallpaper crops on restore"
  bug: "332937943"
  metadata {
    purpose: PURPOSE_BUGFIX
  }
}
 No newline at end of file
+252 −88
Original line number Diff line number Diff line
@@ -16,10 +16,13 @@

package com.android.wallpaperbackup;

import static android.app.Flags.fixWallpaperCropsOnRestore;
import static android.app.Flags.liveWallpaperContentHandling;
import static android.app.WallpaperManager.FLAG_LOCK;
import static android.app.WallpaperManager.FLAG_SYSTEM;
import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE;
import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
import static android.app.WallpaperManager.getOrientation;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;

import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_INELIGIBLE;
@@ -51,12 +54,15 @@ import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.View;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
@@ -72,6 +78,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * Backs up and restores wallpaper and metadata related to it.
@@ -236,8 +243,8 @@ public class WallpaperBackupAgent extends BackupAgent {
    private boolean isDeviceConfigChanged(int width, int height, int secondaryWidth,
            int secondaryHeight) {
        Point currentDimensions = getScreenDimensions();
        Display smallerDisplay = getSmallerDisplayIfExists();
        Point currentSecondaryDimensions = smallerDisplay != null ? getRealSize(smallerDisplay) :
        Point smallerDisplay = getSmallerDisplaySizeIfExists();
        Point currentSecondaryDimensions = smallerDisplay != null ? smallerDisplay :
                new Point(0, 0);

        return (currentDimensions.x != width
@@ -256,13 +263,36 @@ public class WallpaperBackupAgent extends BackupAgent {
        final File deviceInfoStage = new File(getFilesDir(), WALLPAPER_BACKUP_DEVICE_INFO_STAGE);

        if (isDeviceConfigChanged) {
            deviceInfoStage.createNewFile();

            // save the dimensions of the device with xml formatting
            Point dimensions = getScreenDimensions();
            Display smallerDisplay = getSmallerDisplayIfExists();
            Point secondaryDimensions = smallerDisplay != null ? getRealSize(smallerDisplay) :
                    new Point(0, 0);
            Point smallerDisplay = getSmallerDisplaySizeIfExists();
            writeDeviceInfoToFile(deviceInfoStage, dimensions, smallerDisplay);

            deviceInfoStage.createNewFile();
            Point secondaryDimensions = smallerDisplay != null ? smallerDisplay : new Point(0, 0);

            SharedPreferences.Editor editor = sharedPrefs.edit();
            editor.putInt(DEVICE_CONFIG_WIDTH, dimensions.x);
            editor.putInt(DEVICE_CONFIG_HEIGHT, dimensions.y);
            editor.putInt(DEVICE_CONFIG_SECONDARY_WIDTH, secondaryDimensions.x);
            editor.putInt(DEVICE_CONFIG_SECONDARY_HEIGHT, secondaryDimensions.y);
            editor.apply();
        }
        if (DEBUG) Slog.v(TAG, "Storing device dimension data");
        backupFile(deviceInfoStage, data);
    }

    /**
     * Write device info to file.
     * @param smallerDisplay Always null if there is only one display. If there are two displays,
     *                       dimensions of the smallest one (in terms of area in pixels).
     */
    @VisibleForTesting void writeDeviceInfoToFile(
            File deviceInfoStage,
            Point dimensions,
            @Nullable Point smallerDisplay
    ) throws IOException {
        FileOutputStream fstream = new FileOutputStream(deviceInfoStage, false);
        TypedXmlSerializer out = Xml.resolveSerializer(fstream);
        out.startDocument(null, true);
@@ -278,11 +308,11 @@ public class WallpaperBackupAgent extends BackupAgent {

        if (smallerDisplay != null) {
            out.startTag(null, "secondarywidth");
                out.text(String.valueOf(secondaryDimensions.x));
            out.text(String.valueOf(smallerDisplay.x));
            out.endTag(null, "secondarywidth");

            out.startTag(null, "secondaryheight");
                out.text(String.valueOf(secondaryDimensions.y));
            out.text(String.valueOf(smallerDisplay.y));
            out.endTag(null, "secondaryheight");
        }

@@ -292,15 +322,6 @@ public class WallpaperBackupAgent extends BackupAgent {
        FileUtils.sync(fstream);
        fstream.close();

            SharedPreferences.Editor editor = sharedPrefs.edit();
            editor.putInt(DEVICE_CONFIG_WIDTH, dimensions.x);
            editor.putInt(DEVICE_CONFIG_HEIGHT, dimensions.y);
            editor.putInt(DEVICE_CONFIG_SECONDARY_WIDTH, secondaryDimensions.x);
            editor.putInt(DEVICE_CONFIG_SECONDARY_HEIGHT, secondaryDimensions.y);
            editor.apply();
        }
        if (DEBUG) Slog.v(TAG, "Storing device dimension data");
        backupFile(deviceInfoStage, data);
    }

    private void backupWallpaperInfoFile(boolean sysOrLockChanged, FullBackupDataOutput data)
@@ -633,9 +654,13 @@ public class WallpaperBackupAgent extends BackupAgent {
            throws IOException {
        if (stage.exists()) {
            if (multiCrop()) {
                // TODO(b/332937943): implement offset adjustment by manually adjusting crop to
                //  adhere to device aspect ratio
                SparseArray<Rect> cropHints = parseCropHints(info, hintTag);
                SparseArray<Rect> newCropHints = null;
                if (fixWallpaperCropsOnRestore()) {
                    Point bitmapSize = getBitmapSize(stage);
                    newCropHints = adjustCropHints(cropHints, bitmapSize, sourceDeviceDimensions);
                }
                cropHints = fixWallpaperCropsOnRestore() ? newCropHints : cropHints;
                if (cropHints != null) {
                    Slog.i(TAG, "Got restored wallpaper; applying which=" + which
                            + "; cropHints = " + cropHints);
@@ -679,7 +704,7 @@ public class WallpaperBackupAgent extends BackupAgent {
                        if (isTargetMoreNarrowThanSource(targetDeviceDimensions,
                                sourceDeviceSize)) {
                            Rect adjustedCrop = findNewCropfromOldCrop(cropHint,
                                    sourceDeviceDimensions.first, true, targetDeviceDimensions,
                                    sourceDeviceDimensions.first, targetDeviceDimensions,
                                    bitmapSize, true);

                            cropHint.set(adjustedCrop);
@@ -706,6 +731,89 @@ public class WallpaperBackupAgent extends BackupAgent {
        }
    }

    @VisibleForTesting Point getBitmapSize(File stage) throws IOException {
        Point bitmapSize = null;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        try (ParcelFileDescriptor pdf = ParcelFileDescriptor.open(stage,
                MODE_READ_ONLY)) {
            if (pdf != null) {
                BitmapFactory.decodeFileDescriptor(pdf.getFileDescriptor(),
                        null, options);
                bitmapSize = new Point(options.outWidth, options.outHeight);
            }
        }
        return bitmapSize;
    }

    /**
     * Adjust some cropHints restored from the previous device so that they better match the new
     * device dimensions. Overall, the goal is to preserve the same center of the image for the
     * left-most launcher page (or right-most if the device is RTL). If possible, this adjustment is
     * made by enlarging the crop, not reducing it, and parallax is preserved. Refer to
     * {@link #findNewCropfromOldCrop} for the details.
     *
     * @param cropHints for the previous device, map from {@link WallpaperManager.ScreenOrientation}
     *                  to a sub-region of the image to display for that screen orientation
     * @param bitmapSize the dimensions of the restored bitmap
     * @param sourceDeviceDimensions the device dimensions of the source device as per
     *                  {@link #parseDeviceDimensions}
     */
    private SparseArray<Rect> adjustCropHints(SparseArray<Rect> cropHints, Point bitmapSize,
            Pair<Point, Point> sourceDeviceDimensions) {
        if (cropHints == null || bitmapSize == null) {
            return null;
        }
        boolean hasLargeScreen = false;
        SparseArray<Point> allCurrentDimensions = new SparseArray<>();
        for (WallpaperDisplayInfo displayInfo: getInternalDisplays()) {
            hasLargeScreen |= displayInfo.mIsLargeScreen;
            Point size = displayInfo.mDisplaySize;
            allCurrentDimensions.put(getOrientation(size), size);
            Point rotated = new Point(size.y, size.x);
            allCurrentDimensions.put(getOrientation(rotated), rotated);
        }

        SparseArray<Point> allSourceDimensions = new SparseArray<>();
        for (Point size: List.of(sourceDeviceDimensions.first, sourceDeviceDimensions.second)) {
            if (size == null || size.x == 0 || size.y == 0) {
                continue;
            }
            allSourceDimensions.put(getOrientation(size), size);
            Point rotated = new Point(size.y, size.x);
            allSourceDimensions.put(getOrientation(rotated), rotated);
        }

        boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
                == View.LAYOUT_DIRECTION_RTL;

        SparseArray<Rect> adjustedCropHints = new SparseArray<>();
        for (int i = 0; i < cropHints.size(); i++) {
            int orientation = cropHints.keyAt(i);

            // Drop the LANDSCAPE crop hint unless the device has a large screen.
            if (!hasLargeScreen && orientation == ORIENTATION_LANDSCAPE) {
                continue;
            }
            Point currentDimensions = allCurrentDimensions.get(orientation);
            Point sourceDimensions = allSourceDimensions.get(orientation);
            if (currentDimensions == null || sourceDimensions == null) {
                continue;
            }
            Rect oldCrop = cropHints.valueAt(i);
            if (oldCrop.isEmpty() || oldCrop.left < 0 || oldCrop.top < 0
                    || oldCrop.right > bitmapSize.x
                    || oldCrop.bottom > bitmapSize.y) {
                Slog.w(TAG, "Skipping invalid crop " + oldCrop + " for orientation"
                        + orientation + " and bitmap size " + bitmapSize + ".");
            }
            Rect newCrop = findNewCropfromOldCrop(oldCrop, sourceDimensions,
                    currentDimensions, bitmapSize, rtl);
            adjustedCropHints.put(orientation, newCrop);
        }
        return adjustedCropHints;
    }

    /**
     * This method computes the crop of the stored wallpaper to preserve its center point as the
     * user had set it in the previous device.
@@ -716,9 +824,10 @@ public class WallpaperBackupAgent extends BackupAgent {
     * (i.e. space left over on the horizontal axis) to add parallax effect. Parallax is only added
     * if was present in the old device's settings.
     */
    private Rect findNewCropfromOldCrop(Rect oldCrop, Point oldDisplaySize, boolean oldRtl,
    private Rect findNewCropfromOldCrop(Rect oldCrop, Point oldDisplaySize,
            Point newDisplaySize, Point bitmapSize, boolean newRtl) {
        Rect cropWithoutParallax = withoutParallax(oldCrop, oldDisplaySize, oldRtl, bitmapSize);
        Rect cropWithoutParallax = withoutParallax(oldCrop, oldDisplaySize, newRtl, bitmapSize);
        // TODO (b/281648899) this is never empty when multi_crop is true
        oldCrop = oldCrop.isEmpty() ? new Rect(0, 0, bitmapSize.x, bitmapSize.y) : oldCrop;
        float oldParallaxAmount = ((float) oldCrop.width() / cropWithoutParallax.width()) - 1;

@@ -753,7 +862,6 @@ public class WallpaperBackupAgent extends BackupAgent {
            Slog.d(TAG, "- minAcceptableParallax: " + minAcceptableParallax);
            Slog.d(TAG, "- oldCrop: " + oldCrop);
            Slog.d(TAG, "- oldDisplaySize: " + oldDisplaySize);
            Slog.d(TAG, "- oldRtl: " + oldRtl);
            Slog.d(TAG, "- newDisplaySize: " + newDisplaySize);
            Slog.d(TAG, "- bitmapSize: " + bitmapSize);
            Slog.d(TAG, "- newRtl: " + newRtl);
@@ -785,6 +893,7 @@ public class WallpaperBackupAgent extends BackupAgent {
     */
    Rect withoutParallax(Rect crop, Point displaySize, boolean rtl, Point bitmapSize) {
        // in the case an image's crop is not set, we assume the image itself is cropped
        // TODO (b/281648899) this is never empty when multi_crop is true
        if (crop.isEmpty()) {
            crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
        }
@@ -824,19 +933,21 @@ public class WallpaperBackupAgent extends BackupAgent {
    }

    /**
     * This method computes a new crop based on the given crop in order to preserve the center point
     * of the given crop on the provided displaySize. This is only for the case where the device
     * displaySize has a smaller aspect ratio than the cropped image.
     *
     * NOTE: If the width to height ratio is less in the device display than cropped image
     * this means the aspect ratios are off and there will be distortions in the image
     * if the image is applied to the current display (i.e. the image will be skewed ->
     * pixels in the image will not align correctly with the same pixels in the image that are
     * above them)
     * This method adjusts a given crop to match the aspect ratio of a new displaySize. The rules
     * are, in order of priority:
     * <ul>
     *   <li> Preserve the same horizontal center: if the crop needs to be enlarged horizontally,
     *   always add the same amount of width on both sides of the crop.</li>
     *   <li> Do not remove content: when possible, adjust by making the crop wider or taller, not
     *   shorter. Only make the crop shorter when it reaches the border of the image.
     *   <li> Preserve the same vertical center: if the crop needs to be enlarged vertically, add
     *   the same amount of height on both sides when possible.
     * </ul>
     */
    Rect sameCenter(Point displaySize, Point bitmapSize, Rect crop) {

        // in the case an image's crop is not set, we assume the image itself is cropped
        // TODO (b/281648899) this is never empty if multi_crop is true
        if (crop.isEmpty()) {
            crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
        }
@@ -846,7 +957,7 @@ public class WallpaperBackupAgent extends BackupAgent {

        Rect adjustedCrop = new Rect(crop);

        if (screenRatio < cropRatio) {
        if (screenRatio <= cropRatio) {
            // the screen is more narrow than the image, and as such, the image will need to be
            // zoomed in till it fits in the vertical axis. Due to this, we need to manually adjust
            // the image's crop in order for it to fit into the screen without having the framework
@@ -858,11 +969,34 @@ public class WallpaperBackupAgent extends BackupAgent {
            // ratio.
            int heightToAdd = (int) (0.5f + crop.width() / screenRatio - crop.height());

            if (fixWallpaperCropsOnRestore()) {
                int availableHeight = bitmapSize.y - crop.height();
                if (availableHeight >= heightToAdd) {
                    // If there is enough height available to match the new aspect ratio, add that
                    // height to the crop, if possible on both sides of the crop.
                    int heightToAddTop = heightToAdd / 2;
                    int heightToAddBottom = heightToAdd / 2 + heightToAdd % 2;

                    if (crop.top < heightToAddTop) {
                        heightToAddBottom += (heightToAddTop - crop.top);
                        heightToAddTop = crop.top;
                    } else if (bitmapSize.y - crop.bottom < heightToAddBottom) {
                        heightToAddTop += (heightToAddBottom - (bitmapSize.y - crop.bottom));
                        heightToAddBottom = bitmapSize.y - crop.bottom;
                    }
                    adjustedCrop.top -= heightToAddTop;
                    adjustedCrop.bottom += heightToAddBottom;
                } else {
                    // Otherwise, make the crop use the whole bitmap height.
                    adjustedCrop.top = 0;
                    adjustedCrop.bottom = bitmapSize.y;
                }
            } else {
                // Calculate how much extra image space available that can be used to adjust
                // the crop. If this amount is less than heightToAdd, from above, then that means we
            // can't use heightToAdd. Instead we will need to use the maximum possible height, which
            // is the height of the original bitmap. NOTE: the bitmap height may be different than
            // the crop.
                // can't use heightToAdd. Instead we will need to use the maximum possible height,
                // which is the height of the original bitmap. NOTE: the bitmap height may be
                // different than the crop.
                // since there is no guarantee to have height available on both sides
                // (e.g. the available height might be fully at the bottom), grab the minimum
                int availableHeight = 2 * Math.min(crop.top, bitmapSize.y - crop.bottom);
@@ -871,6 +1005,7 @@ public class WallpaperBackupAgent extends BackupAgent {
                // half of the additional height is added to the top and bottom of the crop
                adjustedCrop.top -= actualHeightToAdd / 2 + actualHeightToAdd % 2;
                adjustedCrop.bottom += actualHeightToAdd / 2;
            }

            // Calculate the width of the adjusted crop. Initially we used the fixed width of the
            // crop to calculate the heightToAdd, but since this height may be invalid (based on
@@ -889,15 +1024,29 @@ public class WallpaperBackupAgent extends BackupAgent {
                Slog.d(TAG, "cropRatio: " + cropRatio);
                Slog.d(TAG, "screenRatio: " + screenRatio);
                Slog.d(TAG, "heightToAdd: " + heightToAdd);
                Slog.d(TAG, "actualHeightToAdd: " + actualHeightToAdd);
                Slog.d(TAG, "availableHeight: " + availableHeight);
                Slog.d(TAG, "widthToRemove: " + widthToRemove);
                Slog.d(TAG, "adjustedCrop: " + adjustedCrop);
            }
        } else if (fixWallpaperCropsOnRestore()) {
            // Similar to the case above; but we always to add the same amount of width on both
            // sides to make sure we preserve the center horizontally.
            int widthToAdd = (int) (crop.height() * screenRatio - crop.width());

            return adjustedCrop;
        }
            // In this case, the available width is twice the width available on the shorter side
            int availableWidth = 2 * Math.min(crop.left, bitmapSize.x - crop.right);
            int actualWidthToAdd = Math.min(widthToAdd, availableWidth);
            adjustedCrop.left -= actualWidthToAdd / 2 + actualWidthToAdd % 2;
            adjustedCrop.right += actualWidthToAdd / 2;

            // If we couldn't add enough width to match the new aspect ratio, remove height
            int heightToRemove = (int) (0.5f + crop.height() - adjustedCrop.width() / screenRatio);

            int heightToRemoveTop = heightToRemove / 2;
            int heightToRemoveBottom = heightToRemove / 2 + heightToRemove % 2;

            adjustedCrop.top += heightToRemoveTop;
            adjustedCrop.bottom -= heightToRemoveBottom;
        }
        return adjustedCrop;
    }

@@ -1177,11 +1326,11 @@ public class WallpaperBackupAgent extends BackupAgent {
     * @return a @{Point} object that contains the dimensions of the largest display on the device
     */
    private Point getScreenDimensions() {
        Point largetDimensions = null;
        Point largestDimensions = null;
        int maxArea = 0;

        for (Display display : getInternalDisplays()) {
            Point displaySize = getRealSize(display);
        for (WallpaperDisplayInfo displayInfo : getInternalDisplays()) {
            Point displaySize = displayInfo.mDisplaySize;

            int width = displaySize.x;
            int height = displaySize.y;
@@ -1189,34 +1338,28 @@ public class WallpaperBackupAgent extends BackupAgent {

            if (area > maxArea) {
                maxArea = area;
                largetDimensions = displaySize;
                largestDimensions = displaySize;
            }
        }

        return largetDimensions;
    }

    private Point getRealSize(Display display) {
        DisplayInfo displayInfo = new DisplayInfo();
        display.getDisplayInfo(displayInfo);
        return new Point(displayInfo.logicalWidth, displayInfo.logicalHeight);
        return largestDimensions;
    }

    /**
     * This method returns the smaller display on a multi-display device
     *
     * @return Display that corresponds to the smaller display on a device or null if ther is only
     * @return Display that corresponds to the smaller display on a device or null if there is only
     * one Display on a device
     */
    private Display getSmallerDisplayIfExists() {
        List<Display> internalDisplays = getInternalDisplays();
    @Nullable
    private Point getSmallerDisplaySizeIfExists() {
        List<WallpaperDisplayInfo> internalDisplays = getInternalDisplays();
        Point largestDisplaySize = getScreenDimensions();

        // Find the first non-matching internal display
        for (Display display : internalDisplays) {
            Point displaySize = getRealSize(display);
        for (WallpaperDisplayInfo displayInfo : internalDisplays) {
            Point displaySize = displayInfo.mDisplaySize;
            if (displaySize.x != largestDisplaySize.x || displaySize.y != largestDisplaySize.y) {
                return display;
                return displaySize;
            }
        }

@@ -1224,21 +1367,42 @@ public class WallpaperBackupAgent extends BackupAgent {
        return null;
    }

    static class WallpaperDisplayInfo {
        Point mDisplaySize;
        boolean mIsLargeScreen;

        WallpaperDisplayInfo(int width, int height, int dpi) {
            mDisplaySize = new Point(width, height);
            float densityScaleFactor = (float) DisplayMetrics.DENSITY_DEFAULT / dpi;
            mIsLargeScreen = Math.min(mDisplaySize.x, mDisplaySize.y) * densityScaleFactor >= 600;
        }

        @VisibleForTesting WallpaperDisplayInfo(Point displaySize, boolean isLargeScreen) {
            mDisplaySize = displaySize;
            mIsLargeScreen = isLargeScreen;
        }
    }

    /**
     * This method retrieves the collection of Display objects available in the device.
     * i.e. non-external displays are ignored
     *
     * @return list of displays corresponding to each display in the device
     */
    private List<Display> getInternalDisplays() {
    @VisibleForTesting List<WallpaperDisplayInfo> getInternalDisplays() {
        Display[] allDisplays = mDisplayManager.getDisplays(
                DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);

        List<Display> internalDisplays = new ArrayList<>();
        List<WallpaperDisplayInfo> internalDisplays = new ArrayList<>();
        DisplayInfo outDisplayInfo = new DisplayInfo();
        for (Display display : allDisplays) {
            if (display.getType() == Display.TYPE_INTERNAL) {
                internalDisplays.add(display);
                display.getDisplayInfo(outDisplayInfo);
            }
            internalDisplays.add(new WallpaperDisplayInfo(
                    outDisplayInfo.logicalWidth,
                    outDisplayInfo.logicalHeight,
                    outDisplayInfo.logicalDensityDpi));
        }
        return internalDisplays;
    }