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

Commit e2da4890 authored by Mina Granic's avatar Mina Granic
Browse files

Make camera compat freeform updates passive.

This allows to follow the same flow as other configuration updates -
previously `CameraCompatFreeformPolicy` would manualy update fields
it cares about and force notifying the listeners. Update with the
rest of the configuration makes it more consistent.

Flag: com.android.window.flags.enable_camera_compat_for_desktop_windowing
Fixes: 347864073
Fixes: 369082932
Test: atest WmTests:CameraCompatFreeformPolicyTests
Change-Id: Ib9d76ec4e4d8c3b7ba57b81c6da2c63468c54379
parent 8ab1300f
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.CameraCompatTaskInfo;
import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.res.Configuration;
import android.widget.Toast;
@@ -250,6 +251,14 @@ class AppCompatCameraPolicy {
                cameraCompatFreeformPolicyAspectRatio);
    }

    @CameraCompatTaskInfo.FreeformCameraCompatMode
    static int getCameraCompatFreeformMode(@NonNull ActivityRecord activity) {
        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
        return cameraPolicy != null && cameraPolicy.mCameraCompatFreeformPolicy != null
                ? cameraPolicy.mCameraCompatFreeformPolicy.getCameraCompatMode(activity)
                : CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
    }

    /**
     * Whether we should apply the min aspect ratio per-app override only when an app is connected
     * to the camera.
+2 −2
Original line number Diff line number Diff line
@@ -185,8 +185,8 @@ final class AppCompatUtils {
                        && aspectRatioOverrides.shouldEnableUserAspectRatioSettings();
        appCompatTaskInfo.setEligibleForUserAspectRatioButton(eligibleForAspectRatioButton);
        appCompatTaskInfo.setTopActivityLetterboxed(top.areBoundsLetterboxed());
        appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode = top.mAppCompatController
                .getAppCompatCameraOverrides().getFreeformCameraCompatMode();
        appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode =
                AppCompatCameraPolicy.getCameraCompatFreeformMode(top);
        appCompatTaskInfo.setHasMinAspectRatioOverride(top.mAppCompatController
                .getDesktopAppCompatAspectRatioPolicy().hasMinAspectRatioOverride(task));
    }
+58 −37
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_LANDSCAPE_
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT;
import static android.app.WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS;
import static android.app.WindowConfiguration.WINDOW_CONFIG_DISPLAY_ROTATION;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
@@ -35,6 +37,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.CameraCompatTaskInfo;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.view.DisplayInfo;
@@ -64,8 +67,6 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
    @NonNull
    private final CameraStateMonitor mCameraStateMonitor;

    private boolean mIsCameraCompatTreatmentPending = false;

    @Nullable
    private Task mCameraTask;

@@ -100,13 +101,27 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
        return mIsRunning;
    }

    // Refreshing only when configuration changes after rotation or camera split screen aspect ratio
    // treatment is enabled.
    // Refreshing only when configuration changes after applying camera compat treatment.
    @Override
    public boolean shouldRefreshActivity(@NonNull ActivityRecord activity,
            @NonNull Configuration newConfig,
            @NonNull Configuration lastReportedConfig) {
        return isTreatmentEnabledForActivity(activity) && mIsCameraCompatTreatmentPending;
        return isTreatmentEnabledForActivity(activity, /* shouldCheckOrientation= */ true)
                && haveCameraCompatAttributesChanged(newConfig, lastReportedConfig);
    }

    private boolean haveCameraCompatAttributesChanged(@NonNull Configuration newConfig,
            @NonNull Configuration lastReportedConfig) {
        // Camera compat treatment changes the following:
        // - Letterboxes app bounds to camera compat aspect ratio in app's requested orientation,
        // - Changes display rotation so it matches what the app expects in its chosen orientation,
        // - Rotate-and-crop camera feed to match that orientation (this changes iff the display
        //     rotation changes, so no need to check).
        final long diff = newConfig.windowConfiguration.diff(lastReportedConfig.windowConfiguration,
                /* compareUndefined= */ true);
        final boolean appBoundsChanged = (diff & WINDOW_CONFIG_APP_BOUNDS) != 0;
        final boolean displayRotationChanged = (diff & WINDOW_CONFIG_DISPLAY_ROTATION) != 0;
        return appBoundsChanged || displayRotationChanged;
    }

    /**
@@ -132,22 +147,26 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
    @Override
    public void onCameraOpened(@NonNull ActivityRecord cameraActivity,
            @NonNull String cameraId) {
        if (!isTreatmentEnabledForActivity(cameraActivity)) {
        // Do not check orientation outside of the config recompute, as the app's orientation intent
        // might be obscured by a fullscreen override. Especially for apps which have a camera
        // functionality which is not the main focus of the app: while most of the app might work
        // well in fullscreen, often the camera setup still assumes it will run on a portrait device
        // in its natural orientation and comes out stretched or sideways.
        // Config recalculation will later check the original orientation to avoid applying
        // treatment to apps optimized for large screens.
        if (!isTreatmentEnabledForActivity(cameraActivity, /* shouldCheckOrientation= */ false)) {
            return;
        }
        final int existingCameraCompatMode = cameraActivity.mAppCompatController
                .getAppCompatCameraOverrides()
                        .getFreeformCameraCompatMode();
        final int newCameraCompatMode = getCameraCompatMode(cameraActivity);
        if (newCameraCompatMode != existingCameraCompatMode) {
            mIsCameraCompatTreatmentPending = true;
            mCameraTask = cameraActivity.getTask();
            cameraActivity.mAppCompatController.getAppCompatCameraOverrides()
                    .setFreeformCameraCompatMode(newCameraCompatMode);
            forceUpdateActivityAndTask(cameraActivity);
        } else {
            mIsCameraCompatTreatmentPending = false;

        cameraActivity.recomputeConfiguration();
        updateCameraCompatMode(cameraActivity);
        cameraActivity.getTask().dispatchTaskInfoChangedIfNeeded(/* force= */ true);
        cameraActivity.ensureActivityConfiguration(/* ignoreVisibility= */ false);
    }

    private void updateCameraCompatMode(@NonNull ActivityRecord cameraActivity) {
        cameraActivity.mAppCompatController.getAppCompatCameraOverrides()
                .setFreeformCameraCompatMode(getCameraCompatMode(cameraActivity));
    }

    @Override
@@ -167,7 +186,6 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
            }
        }
        mCameraTask = null;
        mIsCameraCompatTreatmentPending = false;
        return true;
    }

@@ -184,13 +202,12 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
        // Camera compat should direct aspect ratio when in camera compat mode, unless an app has a
        // different camera compat aspect ratio set: this allows per-app camera compat override
        // aspect ratio to be smaller than the default.
        return isInCameraCompatMode(activity) && !activity.mAppCompatController
        return isInFreeformCameraCompatMode(activity) && !activity.mAppCompatController
                .getAppCompatCameraOverrides().isOverrideMinAspectRatioForCameraEnabled();
    }

    private boolean isInCameraCompatMode(@NonNull ActivityRecord activity) {
        return activity.mAppCompatController.getAppCompatCameraOverrides()
                .getFreeformCameraCompatMode() != CAMERA_COMPAT_FREEFORM_NONE;
    boolean isInFreeformCameraCompatMode(@NonNull ActivityRecord activity) {
        return getCameraCompatMode(activity) != CAMERA_COMPAT_FREEFORM_NONE;
    }

    float getCameraCompatAspectRatio(@NonNull ActivityRecord activityRecord) {
@@ -201,16 +218,11 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
        return MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
    }

    private void forceUpdateActivityAndTask(ActivityRecord cameraActivity) {
        cameraActivity.recomputeConfiguration();
        cameraActivity.updateReportedConfigurationAndSend();
        Task cameraTask = cameraActivity.getTask();
        if (cameraTask != null) {
            cameraTask.dispatchTaskInfoChangedIfNeeded(/* force= */ true);
        }
    @CameraCompatTaskInfo.FreeformCameraCompatMode
    int getCameraCompatMode(@NonNull ActivityRecord topActivity) {
        if (!isTreatmentEnabledForActivity(topActivity, /* shouldCheckOrientation= */ true)) {
            return CAMERA_COMPAT_FREEFORM_NONE;
        }

    private static int getCameraCompatMode(@NonNull ActivityRecord topActivity) {
        final int appOrientation = topActivity.getRequestedConfigurationOrientation();
        // It is very important to check the original (actual) display rotation, and not the
        // sandboxed rotation that camera compat treatment sets.
@@ -250,15 +262,24 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
     * <ul>
     *     <li>Treatment is enabled.
     *     <li>Camera is active for the package.
     *     <li>The app has a fixed orientation.
     *     <li>The app has a fixed orientation if {@code checkOrientation} is true.
     *     <li>The app is in freeform windowing mode.
     * </ul>
     *
     * @param checkOrientation Whether to take apps orientation into account for this check. Only
     *                         fixed-orientation apps should be targeted, but this might be
     *                         obscured by OEMs via fullscreen override and the app's original
     *                         intent inaccessible when the camera opens. Thus, policy would pass
     *                         {@code false} here when considering whether to trigger config
     *                         recalculation, and later pass {@code true} during recalculation.
     */
    private boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) {
    @VisibleForTesting
    boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity,
            boolean checkOrientation) {
        int orientation = activity.getRequestedConfigurationOrientation();
        return isCameraCompatForFreeformEnabledForActivity(activity)
                && mCameraStateMonitor.isCameraRunningForActivity(activity)
                && orientation != ORIENTATION_UNDEFINED
                && (!checkOrientation || orientation != ORIENTATION_UNDEFINED)
                && activity.inFreeformWindowingMode()
                // "locked" and "nosensor" values are often used by camera apps that can't
                // handle dynamic changes so we shouldn't force-letterbox them.
@@ -270,7 +291,7 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa

    private boolean isActivityForCameraIdRefreshing(@NonNull ActivityRecord topActivity,
            @NonNull String cameraId) {
        if (!isTreatmentEnabledForActivity(topActivity)
        if (!isTreatmentEnabledForActivity(topActivity, /* checkOrientation= */ true)
                || mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
            return false;
        }
+45 −4
Original line number Diff line number Diff line
@@ -18,16 +18,26 @@ package com.android.server.wm;

import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;

import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
import android.app.TaskInfo;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.DisplayInfo;
import android.view.Surface;

import androidx.annotation.NonNull;

import com.android.window.flags.Flags;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -174,9 +184,13 @@ public class AppCompatUtilsTest extends WindowTestsBase {
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void getTaskInfoPropagatesCameraCompatMode() {
        runTestScenario((robot) -> {
            robot.applyOnActivity(AppCompatActivityRobot::createActivityWithComponentInNewTask);
            robot.dw().allowEnterDesktopMode(/* isAllowed= */ true);
            robot.applyOnActivity(
                    AppCompatActivityRobot::createActivityWithComponentInNewTaskAndDisplay);
            robot.setCameraCompatTreatmentEnabledForActivity(/* enabled= */ true);

            robot.setFreeformCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
            robot.checkTaskInfoFreeformCameraCompatMode(
@@ -212,6 +226,15 @@ public class AppCompatUtilsTest extends WindowTestsBase {
            spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy());
        }

        @Override
        void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
            super.onPostDisplayContentCreation(displayContent);
            mockPortraitDisplay(displayContent);
            if (displayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) {
                spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
            }
        }

        void transparentActivity(@NonNull Consumer<AppCompatTransparentActivityRobot> consumer) {
            // We always create at least an opaque activity in a Task.
            activity().createNewTaskWithBaseActivity();
@@ -235,8 +258,8 @@ public class AppCompatUtilsTest extends WindowTestsBase {
        }

        void setFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) {
            activity().top().mAppCompatController.getAppCompatCameraOverrides()
                    .setFreeformCameraCompatMode(mode);
            doReturn(mode).when(activity().top().mDisplayContent.mAppCompatCameraPolicy
                    .mCameraCompatFreeformPolicy).getCameraCompatMode(activity().top());
        }

        void checkTopActivityLetterboxReason(@NonNull String expected) {
@@ -258,6 +281,24 @@ public class AppCompatUtilsTest extends WindowTestsBase {
            Assert.assertEquals(mode, getTopTaskInfo().appCompatTaskInfo
                    .cameraCompatTaskInfo.freeformCameraCompatMode);
        }

        void setCameraCompatTreatmentEnabledForActivity(boolean enabled) {
            doReturn(enabled).when(activity().displayContent().mAppCompatCameraPolicy
                    .mCameraCompatFreeformPolicy).isTreatmentEnabledForActivity(
                            eq(activity().top()), anyBoolean());
        }

        private void mockPortraitDisplay(DisplayContent displayContent) {
            doAnswer(invocation -> {
                DisplayInfo displayInfo = new DisplayInfo();
                displayContent.getDisplay().getDisplayInfo(displayInfo);
                displayInfo.rotation = Surface.ROTATION_90;
                // Set height and width so that the natural orientation (when rotation is 0) is
                // portrait.
                displayInfo.logicalHeight = 600;
                displayInfo.logicalWidth =  800;
                return displayInfo;
            }).when(displayContent.mWmService.mDisplayManagerInternal).getDisplayInfo(anyInt());
        }
    }
}
+58 −5
Original line number Diff line number Diff line
@@ -223,10 +223,13 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
        setDisplayRotation(Surface.ROTATION_270);

        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
        callOnActivityConfigurationChanging(mActivity);
        callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
                /* lastLetterbox= */ false);
        mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
        callOnActivityConfigurationChanging(mActivity);
        // Activity is letterboxed from the previous configuration change.
        callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
                /* lastLetterbox= */ true);

        assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
        assertActivityRefreshRequested(/* refreshRequested */ true);
@@ -262,6 +265,48 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
                mActivity));
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testShouldRefreshActivity_appBoundsChanged_returnsTrue() {
        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
        Configuration oldConfiguration = createConfiguration(/* letterbox= */ false);
        Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);

        assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
                oldConfiguration));
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testShouldRefreshActivity_displayRotationChanged_returnsTrue() {
        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
        Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
        Configuration newConfiguration = createConfiguration(/* letterbox= */ true);

        oldConfiguration.windowConfiguration.setDisplayRotation(0);
        newConfiguration.windowConfiguration.setDisplayRotation(90);
        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);

        assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
                oldConfiguration));
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testShouldRefreshActivity_appBoundsNorDisplayChanged_returnsFalse() {
        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
        Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
        Configuration newConfiguration = createConfiguration(/* letterbox= */ true);

        oldConfiguration.windowConfiguration.setDisplayRotation(0);
        newConfiguration.windowConfiguration.setDisplayRotation(0);
        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);

        assertFalse(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
                oldConfiguration));
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
@@ -306,6 +351,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() {
        configureActivity(SCREEN_ORIENTATION_FULL_USER);

@@ -318,6 +364,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() {
        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
        final float configAspectRatio = 1.5f;
@@ -331,8 +378,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
                /* delta= */ 0.001);
    }


    @Test
    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
    public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() {
        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
        final float configAspectRatio = 1.5f;
@@ -411,9 +458,15 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
    }

    private void callOnActivityConfigurationChanging(ActivityRecord activity) {
        callOnActivityConfigurationChanging(activity, /* letterboxNew= */ true,
                /* lastLetterbox= */false);
    }

    private void callOnActivityConfigurationChanging(ActivityRecord activity, boolean letterboxNew,
            boolean lastLetterbox) {
        mActivityRefresher.onActivityConfigurationChanging(activity,
                /* newConfig */ createConfiguration(/*letterbox=*/ true),
                /* lastReportedConfig */ createConfiguration(/*letterbox=*/ false));
                /* newConfig */ createConfiguration(letterboxNew),
                /* lastReportedConfig */ createConfiguration(lastLetterbox));
    }

    private Configuration createConfiguration(boolean letterbox) {