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

Commit 68679266 authored by Jiaming Liu's avatar Jiaming Liu
Browse files

Use rotation from WindowConfiguration to rotate FoldingFeature to avoid race condition

We used DisplayInfo.rotation to rotate the FoldingFeature while using WindowConfiguration to validate the rotated FoldingFeature. Based on logs in b/295277561 and b/297215998, the rotations in WindowConfiguration and DisplayInfo were out of sync and caused validation failure and app crash. Using the
rotation from WindowConfiguration for feature rotation should prevent this issue.

Bug: 295277561
Bug: 297215998
Bug: 297153447
Test: atest CtsWindowManagerJetpackTestCases
Test: atest WMJetpackUnitTests:ExtensionHelperTest
Change-Id: Iedb1365d97bcfe14643199a4000472aadeee8282
parent c567baf4
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -319,13 +319,17 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
            return features;
        }

        // We will transform the feature bounds to the Activity window, so using the rotation
        // from the same source (WindowConfiguration) to make sure they are synchronized.
        final int rotation = windowConfiguration.getDisplayRotation();

        for (CommonFoldingFeature baseFeature : storedFeatures) {
            Integer state = convertToExtensionState(baseFeature.getState());
            if (state == null) {
                continue;
            }
            Rect featureRect = baseFeature.getRect();
            rotateRectToDisplayRotation(displayId, featureRect);
            rotateRectToDisplayRotation(displayId, rotation, featureRect);
            transformToWindowSpaceRect(windowConfiguration, featureRect);

            if (isZero(featureRect)) {
+3 −1
Original line number Diff line number Diff line
@@ -120,10 +120,12 @@ class SampleSidecarImpl extends StubSidecar {
        }

        List<SidecarDisplayFeature> features = new ArrayList<>();
        final int rotation = activity.getResources().getConfiguration().windowConfiguration
                .getDisplayRotation();
        for (CommonFoldingFeature baseFeature : mStoredFeatures) {
            SidecarDisplayFeature feature = new SidecarDisplayFeature();
            Rect featureRect = baseFeature.getRect();
            rotateRectToDisplayRotation(displayId, featureRect);
            rotateRectToDisplayRotation(displayId, rotation, featureRect);
            transformToWindowSpaceRect(activity, featureRect);
            feature.setRect(featureRect);
            feature.setType(baseFeature.getType());
+32 −38
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

package androidx.window.util;

import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;

@@ -25,12 +23,14 @@ import android.app.WindowConfiguration;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.util.RotationUtils;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.UiContext;
import androidx.annotation.VisibleForTesting;

/**
 * Util class for both Sidecar and Extensions.
@@ -44,47 +44,41 @@ public final class ExtensionHelper {
    /**
     * Rotates the input rectangle specified in default display orientation to the current display
     * rotation.
     *
     * @param displayId the display id.
     * @param rotation the target rotation relative to the default display orientation.
     * @param inOutRect the input/output Rect as specified in the default display orientation.
     */
    public static void rotateRectToDisplayRotation(int displayId, Rect inOutRect) {
        DisplayManagerGlobal dmGlobal = DisplayManagerGlobal.getInstance();
        DisplayInfo displayInfo = dmGlobal.getDisplayInfo(displayId);
        int rotation = displayInfo.rotation;

        boolean isSideRotation = rotation == ROTATION_90 || rotation == ROTATION_270;
        int displayWidth = isSideRotation ? displayInfo.logicalHeight : displayInfo.logicalWidth;
        int displayHeight = isSideRotation ? displayInfo.logicalWidth : displayInfo.logicalHeight;

        inOutRect.intersect(0, 0, displayWidth, displayHeight);
    public static void rotateRectToDisplayRotation(
            int displayId, @Surface.Rotation int rotation, @NonNull Rect inOutRect) {
        final DisplayManagerGlobal dmGlobal = DisplayManagerGlobal.getInstance();
        final DisplayInfo displayInfo = dmGlobal.getDisplayInfo(displayId);

        rotateBounds(inOutRect, displayWidth, displayHeight, rotation);
        rotateRectToDisplayRotation(displayInfo, rotation, inOutRect);
    }

    /**
     * Rotates the input rectangle within parent bounds for a given delta.
     */
    private static void rotateBounds(Rect inOutRect, int parentWidth, int parentHeight,
            @Surface.Rotation int delta) {
        int origLeft = inOutRect.left;
        switch (delta) {
            case ROTATION_0:
                return;
            case ROTATION_90:
                inOutRect.left = inOutRect.top;
                inOutRect.top = parentWidth - inOutRect.right;
                inOutRect.right = inOutRect.bottom;
                inOutRect.bottom = parentWidth - origLeft;
                return;
            case ROTATION_180:
                inOutRect.left = parentWidth - inOutRect.right;
                inOutRect.right = parentWidth - origLeft;
                return;
            case ROTATION_270:
                inOutRect.left = parentHeight - inOutRect.bottom;
                inOutRect.bottom = inOutRect.right;
                inOutRect.right = parentHeight - inOutRect.top;
                inOutRect.top = origLeft;
                return;
    @VisibleForTesting
    static void rotateRectToDisplayRotation(@NonNull DisplayInfo displayInfo,
            @Surface.Rotation int rotation, @NonNull Rect inOutRect) {
        // The inOutRect is specified in the default display orientation, so here we need to get
        // the display width and height in the default orientation to perform the intersection and
        // rotation.
        final boolean isSideRotation =
                displayInfo.rotation == ROTATION_90 || displayInfo.rotation == ROTATION_270;
        final int baseDisplayWidth =
                isSideRotation ? displayInfo.logicalHeight : displayInfo.logicalWidth;
        final int baseDisplayHeight =
                isSideRotation ? displayInfo.logicalWidth : displayInfo.logicalHeight;

        final boolean success = inOutRect.intersect(0, 0, baseDisplayWidth, baseDisplayHeight);
        if (!success) {
            throw new IllegalArgumentException("inOutRect must intersect with the display."
                    + " inOutRect: " + inOutRect
                    + ", baseDisplayWidth: " + baseDisplayWidth
                    + ", baseDisplayHeight: " + baseDisplayHeight);
        }

        RotationUtils.rotateBounds(inOutRect, baseDisplayWidth, baseDisplayHeight, rotation);
    }

    /** Transforms rectangle from absolute coordinate space to the window coordinate space. */
+1 −1
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Test class for {@link WindowExtensionsTest}.
 * Test class for {@link WindowExtensions}.
 *
 * Build/Install/Run:
 *  atest WMJetpackUnitTests:WindowExtensionsTest
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.window.util;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.view.DisplayInfo;
import android.view.Surface;

import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Test class for {@link ExtensionHelper}.
 *
 * Build/Install/Run:
 *  atest WMJetpackUnitTests:ExtensionHelperTest
 */
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ExtensionHelperTest {

    private static final int MOCK_DISPLAY_HEIGHT = 1000;
    private static final int MOCK_DISPLAY_WIDTH = 2000;
    private static final int MOCK_FEATURE_LEFT = 100;
    private static final int MOCK_FEATURE_RIGHT = 200;

    private static final int[] ROTATIONS = {
            Surface.ROTATION_0,
            Surface.ROTATION_90,
            Surface.ROTATION_180,
            Surface.ROTATION_270
    };

    private static final DisplayInfo[] MOCK_DISPLAY_INFOS = {
            getMockDisplayInfo(Surface.ROTATION_0),
            getMockDisplayInfo(Surface.ROTATION_90),
            getMockDisplayInfo(Surface.ROTATION_180),
            getMockDisplayInfo(Surface.ROTATION_270),
    };

    @Test
    public void testRotateRectToDisplayRotation() {
        for (int rotation : ROTATIONS) {
            final Rect expectedResult = getExpectedFeatureRectAfterRotation(rotation);
            // The method should return correctly rotated Rect even if the requested rotation value
            // differs from the rotation in DisplayInfo. This is because the WindowConfiguration is
            // not always synced with DisplayInfo.
            for (DisplayInfo displayInfo : MOCK_DISPLAY_INFOS) {
                final Rect rect = getMockFeatureRect();
                ExtensionHelper.rotateRectToDisplayRotation(displayInfo, rotation, rect);
                assertEquals(
                        "Result Rect should equal to expected for rotation: " + rotation
                                + "; displayInfo: " + displayInfo,
                        expectedResult, rect);
            }
        }
    }

    @Test
    public void testRotateRectToDisplayRotation_invalidInputRect() {
        final Rect invalidRect = new Rect(
                MOCK_DISPLAY_WIDTH + 10, 0, MOCK_DISPLAY_WIDTH + 10, MOCK_DISPLAY_HEIGHT);
        assertThrows(IllegalArgumentException.class,
                () -> ExtensionHelper.rotateRectToDisplayRotation(
                        MOCK_DISPLAY_INFOS[0], ROTATIONS[0], invalidRect));
    }


    @NonNull
    private static DisplayInfo getMockDisplayInfo(@Surface.Rotation int rotation) {
        final DisplayInfo displayInfo = new DisplayInfo();
        displayInfo.rotation = rotation;
        if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
            displayInfo.logicalWidth = MOCK_DISPLAY_WIDTH;
            displayInfo.logicalHeight = MOCK_DISPLAY_HEIGHT;
        } else {
            displayInfo.logicalWidth = MOCK_DISPLAY_HEIGHT;
            displayInfo.logicalHeight = MOCK_DISPLAY_WIDTH;
        }
        return displayInfo;
    }

    @NonNull
    private static Rect getMockFeatureRect() {
        return new Rect(MOCK_FEATURE_LEFT, 0, MOCK_FEATURE_RIGHT, MOCK_DISPLAY_HEIGHT);
    }

    @NonNull
    private static Rect getExpectedFeatureRectAfterRotation(@Surface.Rotation int rotation) {
        switch (rotation) {
            case Surface.ROTATION_0:
                return new Rect(
                        MOCK_FEATURE_LEFT, 0, MOCK_FEATURE_RIGHT, MOCK_DISPLAY_HEIGHT);
            case Surface.ROTATION_90:
                return new Rect(0, MOCK_DISPLAY_WIDTH - MOCK_FEATURE_RIGHT,
                        MOCK_DISPLAY_HEIGHT, MOCK_DISPLAY_WIDTH - MOCK_FEATURE_LEFT);
            case Surface.ROTATION_180:
                return new Rect(MOCK_DISPLAY_WIDTH - MOCK_FEATURE_RIGHT, 0,
                        MOCK_DISPLAY_WIDTH - MOCK_FEATURE_LEFT, MOCK_DISPLAY_HEIGHT);
            case Surface.ROTATION_270:
                return new Rect(0, MOCK_FEATURE_LEFT, MOCK_DISPLAY_HEIGHT,
                        MOCK_FEATURE_RIGHT);
            default:
                throw new IllegalArgumentException("Unknown rotation value: " + rotation);
        }
    }
}