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

Commit 9380194c authored by Aurélien Pomini's avatar Aurélien Pomini Committed by Android (Google) Code Review
Browse files

Merge "Crop logic for external displays." into main

parents 0006d1a6 7b1cc3ea
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()