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

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

Crop logic for external displays.

Flag: android.app.enable_connected_displays_wallpaper
Bug: 420575760
Test: manual
Test: atest WallpaperCropperTest
Change-Id: I0655dcd747e64b6e8f769b94da57dd243a05caa9
parent 724367e8
Loading
Loading
Loading
Loading
+123 −2
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.view.DisplayInfo;
import android.view.View;
import android.window.DesktopExperienceFlags;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.utils.TimingsTraceAndSlog;
@@ -71,6 +72,13 @@ public class WallpaperCropper {
     */
    @VisibleForTesting static final float MAX_PARALLAX = 1f;

    /**
     * For connected displays, if the width or height of the image is smaller than the width or
     * height of the screen by a factor larger than this amount, the quality is considered too low
     * and a fallback wallpaper should be used instead.
     */
    @VisibleForTesting static final float CONNECTED_DISPLAY_MAX_DISPLAY_TO_IMAGE_RATIO = 1.5f;

    /**
     * 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
@@ -116,6 +124,20 @@ public class WallpaperCropper {
    public static Rect getCrop(Point displaySize, WallpaperDefaultDisplayInfo defaultDisplayInfo,
            Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl) {

        // Case 0: if we're looking for the crop of an external display, use external display logic
        if (DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_WALLPAPER.isTrue()) {
            boolean isExternalDisplay = true;
            for (int i = 0; i < defaultDisplayInfo.defaultDisplaySizes.size(); i++) {
                if (defaultDisplayInfo.defaultDisplaySizes.valueAt(i).equals(displaySize)) {
                    isExternalDisplay = false;
                }
            }
            if (isExternalDisplay) {
                return getCropForExternalDisplay(
                        displaySize, defaultDisplayInfo, bitmapSize, suggestedCrops, rtl);
            }
        }

        int orientation = getOrientation(displaySize);

        // Case 1: if no crops are provided, show the full image (from the left, or right if RTL).
@@ -208,7 +230,6 @@ public class WallpaperCropper {
            return res;
        }


        // 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 = defaultDisplayInfo.getFoldedOrientation(orientation);
@@ -832,6 +853,106 @@ public class WallpaperCropper {
        }
    }

    private static Rect getCropForExternalDisplay(
            Point displaySize,
            WallpaperDefaultDisplayInfo defaultDisplayInfo,
            Point bitmapSize,
            SparseArray<Rect> suggestedCrops,
            boolean rtl) {

        // If no custom crops are provided, center-align the image, with parallax if possible
        if (suggestedCrops == null || suggestedCrops.size() == 0) {
            Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
            return getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD);
        }

        // Otherwise, find the custom crop closest to the external display aspect ratio
        Point closestDisplaySize = null;
        Rect closestCrop = null;
        float minDistance = Float.MAX_VALUE;
        float displayAspectRatio = (float) displaySize.x / displaySize.y;
        for (int i = 0; i < suggestedCrops.size(); i++) {
            int orientation = suggestedCrops.keyAt(i);
            Point suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(orientation);
            if (suggestedDisplaySize != null) {
                float aspectRatio = (float) suggestedDisplaySize.x / suggestedDisplaySize.y;
                float distance = Math.abs((float) Math.log(aspectRatio / displayAspectRatio));
                if (distance < minDistance) {
                    minDistance = distance;
                    closestCrop = suggestedCrops.valueAt(i);
                    closestDisplaySize = suggestedDisplaySize;
                }
            }
        }

        if (closestCrop == null) {
            Slog.w(TAG, "Did not find valid crop to use for external display of size " + displaySize
                    + " and suggestedCrops " + suggestedCrops + ", fallback to center-align.");
            return getCropForExternalDisplay(
                    displaySize,
                    defaultDisplayInfo,
                    bitmapSize,
                    new SparseArray<>(),
                    rtl
            );
        }

        // Compute the visible part of that crop (without parallax)
        Rect noParallax = noParallax(closestCrop, closestDisplaySize, bitmapSize, rtl);

        // Adjust it to match the external display's aspect ratio
        Rect crop = getAdjustedCrop(noParallax, bitmapSize, displaySize, false, rtl, ADD);

        // If the resolution is not good enough, enlarge the crop until we reach the border of the
        // image or until we reach the required resolution
        float displayCropRatio = (float) displaySize.y / crop.height();
        if (displayCropRatio > CONNECTED_DISPLAY_MAX_DISPLAY_TO_IMAGE_RATIO) {
            float targetScale = displayCropRatio / CONNECTED_DISPLAY_MAX_DISPLAY_TO_IMAGE_RATIO;
            int targetWidth = Math.round(crop.width() * targetScale);
            int targetHeight = Math.round(crop.height() * targetScale);
            int actualWidth = Math.min(targetWidth, bitmapSize.x);
            int actualHeight = Math.min(targetHeight, bitmapSize.y);
            float scale = Math.min(
                    (float) actualWidth / crop.width(),
                    (float) actualHeight / crop.height());
            int widthToAdd = Math.round(crop.width() * (scale - 1f));
            int heightToAdd = Math.round(crop.height() * (scale - 1f));
            int widthToAddLeft = widthToAdd / 2;
            int widthToAddRight = widthToAdd - widthToAddLeft;
            int heightToAddTop = heightToAdd / 2;
            int heightToAddBottom = heightToAdd - heightToAddTop;

            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;
            }
            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;
            }
            crop.left -= widthToAddLeft;
            crop.right += widthToAddRight;
            crop.top -= heightToAddTop;
            crop.bottom += heightToAddBottom;
        }

        // Add some more parallax if we can
        if (rtl) {
            crop.left = 0;
        } else {
            crop.right = bitmapSize.x;
        }

        // Finally, use getAdjustedCrop just to make sure we don't exceed MAX_PARALLAX
        return getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD);
    }

    /**
     * Returns true if a wallpaper is compatible with a given display with ID, {@code displayId}.
     *
@@ -877,7 +998,7 @@ public class WallpaperCropper {

        double maxDisplayToImageRatio = Math.max((double) displaySize.x / croppedImageBound.width(),
                (double) displaySize.y / croppedImageBound.height());
        if (maxDisplayToImageRatio > 1.5) {
        if (maxDisplayToImageRatio > CONNECTED_DISPLAY_MAX_DISPLAY_TO_IMAGE_RATIO) {
            return false;
        }

+163 −16
Original line number Diff line number Diff line
@@ -38,9 +38,11 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

import android.app.Flags;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.util.ArraySet;
@@ -101,11 +103,6 @@ public class WallpaperCropperTest {
    private static final Point SQUARE_PORTRAIT_ONE = new Point(1000, 800);
    private static final Point SQUARE_LANDSCAPE_ONE = new Point(800, 1000);

    /**
     * Common device: a single screen of portrait/landscape orientation
     */
    private static final List<Point> STANDARD_DISPLAY = List.of(PORTRAIT_ONE);

    /** 1: folded: portrait, unfolded: square with w < h */
    private static final List<Point> FOLDABLE_ONE = List.of(PORTRAIT_ONE, SQUARE_PORTRAIT_ONE);

@@ -454,7 +451,6 @@ public class WallpaperCropperTest {
     */
    @Test
    public void testGetCrop_noSuggestedCrops() {
        WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
        Point bitmapSize = new Point(800, 1000);
        Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
        SparseArray<Rect> suggestedCrops = new SparseArray<>();
@@ -470,13 +466,14 @@ public class WallpaperCropperTest {

        for (int i = 0; i < displaySizes.size(); i++) {
            Point displaySize = displaySizes.get(i);
            WallpaperDefaultDisplayInfo displayInfo = setUpWithDisplays(List.of(displaySize));
            Point expectedCropSize = expectedCropSizes.get(i);
            for (boolean rtl : List.of(false, true)) {
                Rect expectedCrop = rtl ? rightOf(bitmapRect, expectedCropSize)
                        : leftOf(bitmapRect, expectedCropSize);
                assertThat(
                        WallpaperCropper.getCrop(
                                displaySize, defaultDisplayInfo, bitmapSize, suggestedCrops, rtl))
                                displaySize, displayInfo, bitmapSize, suggestedCrops, rtl))
                        .isEqualTo(expectedCrop);
            }
        }
@@ -489,10 +486,10 @@ public class WallpaperCropperTest {
     */
    @Test
    public void testGetCrop_hasSuggestedCrop() {
        WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
        WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(List.of(PORTRAIT_ONE));
        Point bitmapSize = new Point(800, 1000);
        SparseArray<Rect> suggestedCrops = new SparseArray<>();
        suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 400, 800));
        suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 600, 800));
        for (int otherOrientation: List.of(ORIENTATION_LANDSCAPE, ORIENTATION_SQUARE_LANDSCAPE,
                ORIENTATION_SQUARE_PORTRAIT)) {
            suggestedCrops.put(otherOrientation, new Rect(0, 0, 10, 10));
@@ -500,13 +497,9 @@ public class WallpaperCropperTest {

        for (boolean rtl : List.of(false, true)) {
            assertThat(
                    WallpaperCropper.getCrop(new Point(300, 800), defaultDisplayInfo, bitmapSize,
                            suggestedCrops, rtl))
                    WallpaperCropper.getCrop(PORTRAIT_ONE, defaultDisplayInfo,
                            bitmapSize, suggestedCrops, rtl))
                    .isEqualTo(suggestedCrops.get(ORIENTATION_PORTRAIT));
            assertThat(
                    WallpaperCropper.getCrop(new Point(500, 800), defaultDisplayInfo, bitmapSize,
                            suggestedCrops, rtl))
                    .isEqualTo(new Rect(0, 0, 500, 800));
        }
    }

@@ -521,7 +514,7 @@ public class WallpaperCropperTest {
     */
    @Test
    public void testGetCrop_hasRotatedSuggestedCrop() {
        WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
        WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(FOLDABLE_ONE);
        Point bitmapSize = new Point(2000, 1800);
        Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
        SparseArray<Rect> suggestedCrops = new SparseArray<>();
@@ -726,6 +719,160 @@ public class WallpaperCropperTest {
        }
    }

    /**
     * Test that {@link WallpaperCropper#getCrop}, when called for an external display (i.e. with
     * a display size not in the {@link WallpaperDefaultDisplayInfo}) and with no crop provided,
     * uses the full image similarly to {@link #testGetCrop_noSuggestedCrops()}.
     */
    @Test
    public void testGetCrop_noSuggestedCrops_externalDisplay() {
        WallpaperDefaultDisplayInfo displayInfo = setUpWithDisplays(List.of(PORTRAIT_ONE));
        Point bitmapSize = new Point(800, 1000);
        Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
        SparseArray<Rect> suggestedCrops = new SparseArray<>();

        List<Point> displaySizes = List.of(
                new Point(500, 1000),
                new Point(200, 1000),
                new Point(1000, 500));
        List<Point> expectedCropSizes = List.of(
                new Point(Math.min(800, (int) (500 * (1 + WallpaperCropper.MAX_PARALLAX))), 1000),
                new Point(Math.min(800, (int) (200 * (1 + WallpaperCropper.MAX_PARALLAX))), 1000),
                new Point(800, 400));

        for (int i = 0; i < displaySizes.size(); i++) {
            Point displaySize = displaySizes.get(i);
            Point expectedCropSize = expectedCropSizes.get(i);
            for (boolean rtl : List.of(false, true)) {
                Rect expectedCrop = rtl ? rightOf(bitmapRect, expectedCropSize)
                        : leftOf(bitmapRect, expectedCropSize);
                assertThat(
                        WallpaperCropper.getCrop(
                                displaySize, displayInfo, bitmapSize, suggestedCrops, rtl))
                        .isEqualTo(expectedCrop);
            }
        }
    }

    /**
     * Test that {@link WallpaperCropper#getCrop}, called for an external display with only one
     * suggested crop, properly reuses the suggested crop to get the crop for the external display.
     */
    @Test
    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
    public void testGetCrop_hasOneSuggestedCrop_externalDisplay_usesSuggestedCrop() {
        WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(FOLDABLE_ONE);
        Point bitmapSize = new Point(2000, 2000);
        SparseArray<Rect> suggestedCrops = new SparseArray<>();

        // In this example we have a suggested portrait crop with 200px for parallax
        suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(400, 0, 1600, 1600));

        for (boolean rtl : List.of(false, true)) {
            // Test 1: square screen
            Rect crop = WallpaperCropper.getCrop(new Point(1000, 1000), defaultDisplayInfo,
                    bitmapSize, suggestedCrops, rtl);
            assertThat(crop.top).isEqualTo(0);
            assertThat(crop.bottom).isEqualTo(1600);
            if (rtl) {
                // The crop without parallax of the default display is (600, 1600, 1600).
                // We should add 300px on each side to match the square screen. Then we're allowed
                // to add width for parallax to the left since rtl is true.
                assertThat(crop.left).isAtMost(300);
                assertThat(crop.right).isEqualTo(1900);
            } else {
                // The crop without parallax of the default display is (400, 0, 1400, 1600).
                // We should add 300px on each side to match the square screen. Then we're allowed
                // to add width for parallax to the right since rtl is false.
                assertThat(crop.left).isEqualTo(100);
                assertThat(crop.right).isAtLeast(1500);
            }

            // Test 2: landscape screen
            assertThat(
                    WallpaperCropper.getCrop(new Point(2000, 1000), defaultDisplayInfo,
                            bitmapSize, suggestedCrops, rtl))
                    // We should use all the available width then remove some height on both sides
                    // of the crop to match the landscape screen, regardless of layout direction.
                    .isEqualTo(new Rect(0, 300, 2000, 1300));

            // Test 3: very narrow portrait screen
            crop = WallpaperCropper.getCrop(new Point(100, 1000), defaultDisplayInfo,
                    bitmapSize, suggestedCrops, rtl);
            // We should use all the available height to match the external display aspect ratio.
            assertThat(crop.top).isEqualTo(0);
            assertThat(crop.bottom).isEqualTo(2000);
            if (rtl) {
                // The crop without parallax of the default display is (600, 1600, 1600).
                // We should remove 400px on each side to match the square screen. Then we're
                // allowed to add width for parallax to the left since rtl is true.
                assertThat(crop.left).isAtMost(1000);
                assertThat(crop.right).isEqualTo(1200);
            } else {
                // The crop without parallax of the default display is (400, 0, 1400, 1600).
                // We should remove 400px on each side to match the square screen. Then we're
                // allowed to add width for parallax to the right since rtl is false.
                assertThat(crop.left).isEqualTo(800);
                assertThat(crop.right).isAtLeast(1000);
            }
        }
    }

    /**
     * Test that {@link WallpaperCropper#getCrop}, called for an external display with several
     * suggested crops, uses the suggested crop for the display closest to the external display in
     * terms of aspect ratio.
     */
    @Test
    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
    public void testGetCrop_multipleSuggestedCrops_externalDisplay_usesClosestDisplayAspectRatio() {
        WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(
                List.of(new Point(500, 800), new Point(1000, 800)));
        Point bitmapSize = new Point(3000, 3000);
        SparseArray<Rect> suggestedCrops = new SparseArray<>();
        suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 1, 1));
        suggestedCrops.put(ORIENTATION_LANDSCAPE, new Rect(0, 0, 1, 1));
        suggestedCrops.put(ORIENTATION_SQUARE_LANDSCAPE, new Rect(0, 0, 2250, 1800));

        Rect newCrop = WallpaperCropper.getCrop(new Point(720, 800), defaultDisplayInfo,
                bitmapSize, suggestedCrops, false);

        // The device has a aspect ratios of 0.625 for PORTRAIT and 1.25 for LANDSCAPE. In this test
        // case we're looking for the crop for an aspect ratio of 720/800 = 0.9. We should be using
        // the SQUARE_LANDSCAPE crop since it is multiplicatively closer to the 0.9 aspect ratio,
        // since 1.25 / 0.9 < 0.9 / 0.625. So we should reuse the (0, 0, 2250, 1800) crop. To match
        // the aspect ratio of the new 720 x 800 screen, we should add 700 px of height to the crop.
        assertThat(newCrop.left).isEqualTo(0);
        assertThat(newCrop.top).isEqualTo(0);
        assertThat(newCrop.right).isAtLeast(2250);
        assertThat(newCrop.bottom).isEqualTo(2500);
    }

    /**
     * Test that {@link WallpaperCropper#getCrop}, when called for a large external display and
     * a small suggested crop, enlarges the suggested crop until it reaches the required resolution
     * as per {@link WallpaperCropper#CONNECTED_DISPLAY_MAX_DISPLAY_TO_IMAGE_RATIO}.
     */
    @Test
    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
    public void testGetCrop_externalDisplay_lowResolution_enlargesSuggestedCrop() {
        WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(List.of(PORTRAIT_ONE));
        Point bitmapSize = new Point(10000, 10000);
        SparseArray<Rect> suggestedCrops = new SparseArray<>();
        suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(4900, 4900, 5100, 5100));

        Point newDisplaySize = new Point(10000, 12000);
        Rect newCrop = WallpaperCropper.getCrop(newDisplaySize, defaultDisplayInfo, bitmapSize,
                suggestedCrops, false);

        float displayToImageRatio = WallpaperCropper.CONNECTED_DISPLAY_MAX_DISPLAY_TO_IMAGE_RATIO;
        int minExpectedWidth = (int) (0.5f + newDisplaySize.x / displayToImageRatio);
        int expectedHeight = (int) (0.5f + newDisplaySize.y / displayToImageRatio);
        assertThat(newCrop.width()).isAtLeast(minExpectedWidth - 1);
        assertThat(newCrop.height()).isWithin(1).of(expectedHeight);
        assertThat(newCrop.centerY()).isWithin(1).of(5000);
    }

    // Test isWallpaperCompatibleForDisplay always return true for the default display.
    @Test
    public void isWallpaperCompatibleForDisplay_defaultDisplay_returnTrue()