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

Commit 880dfc1c authored by Mina Granic's avatar Mina Granic
Browse files

[1/n] Turn on camera compat mode for fixed orientation freeform activities when using camera.

Camera compat mode letterboxes camera activities that are likely to be untested on large screens and likely to be broken (fixed-orientation activities). If activated, camera compat mode will cause the camera to rotate and crop the preview to portrait if the camera feed is landscape, and changes camera and display rotation signals to match the natural orientation portrait (future changes).

Bug: 314960895
Test: atest CameraFreeformCompatPolicyTest

Change-Id: I8435fce70ecdef55bebaffd56c4643170138913c
parent 2e63bd90
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
import static android.app.CameraCompatTaskInfo.cameraCompatControlStateToString;
import static android.app.WaitResult.INVALID_DELAY;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
@@ -46,6 +47,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -854,7 +856,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
    @CameraCompatControlState
    private int mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN;
    // The callback that allows to ask the calling View to apply the treatment for stretched
    // issues affecting camera viewfinders when the user clicks on the camera compat control.
    @Nullable
@@ -8549,11 +8550,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
        // and back which can cause visible issues (see b/184078928).
        final int parentWindowingMode =
                newParentConfiguration.windowConfiguration.getWindowingMode();
        final boolean isInCameraCompatFreeform = parentWindowingMode == WINDOWING_MODE_FREEFORM
                && mLetterboxUiController.getFreeformCameraCompatMode()
                        != CAMERA_COMPAT_FREEFORM_NONE;
        // Bubble activities should always fill their parent and should not be letterboxed.
        final boolean isFixedOrientationLetterboxAllowed = !getLaunchedFromBubble()
                && (parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW
                        || parentWindowingMode == WINDOWING_MODE_FULLSCREEN
                        || isInCameraCompatFreeform
                        // When starting to switch between PiP and fullscreen, the task is pinned
                        // and the activity is fullscreen. But only allow to apply letterbox if the
                        // activity is exiting PiP because an entered PiP should fill the task.
+196 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 com.android.server.wm;

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;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.content.res.Configuration.ORIENTATION_UNDEFINED;

import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;

import android.annotation.NonNull;
import android.app.CameraCompatTaskInfo;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.common.ProtoLog;
import com.android.window.flags.Flags;

/**
 * Policy for camera compatibility freeform treatment.
 *
 * <p>The treatment is applied to a fixed-orientation camera activity in freeform windowing mode.
 * The treatment letterboxes or pillarboxes the activity to the expected orientation and provides
 * changes to the camera and display orientation signals to match those expected on a portrait
 * device in that orientation (for example, on a standard phone).
 */
final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompatStateListener,
        ActivityRefresher.Evaluator {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "CameraCompatFreeformPolicy" : TAG_WM;

    @NonNull
    private final DisplayContent mDisplayContent;
    @NonNull
    private final ActivityRefresher mActivityRefresher;
    @NonNull
    private final CameraStateMonitor mCameraStateMonitor;

    private boolean mIsCameraCompatTreatmentPending = false;

    CameraCompatFreeformPolicy(@NonNull DisplayContent displayContent,
            @NonNull CameraStateMonitor cameraStateMonitor,
            @NonNull ActivityRefresher activityRefresher) {
        mDisplayContent = displayContent;
        mCameraStateMonitor = cameraStateMonitor;
        mActivityRefresher = activityRefresher;
    }

    void start() {
        mCameraStateMonitor.addCameraStateListener(this);
        mActivityRefresher.addEvaluator(this);
    }

    /** Releases camera callback listener. */
    void dispose() {
        mCameraStateMonitor.removeCameraStateListener(this);
        mActivityRefresher.removeEvaluator(this);
    }

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

    /**
     * Whether activity is eligible for camera compatibility free-form treatment.
     *
     * <p>The treatment is applied to a fixed-orientation camera activity in free-form windowing
     * mode. The treatment letterboxes or pillarboxes the activity to the expected orientation and
     * provides changes to the camera and display orientation signals to match those expected on a
     * portrait device in that orientation (for example, on a standard phone).
     *
     * <p>The treatment is enabled when the following conditions are met:
     * <ul>
     *     <li>Property gating the camera compatibility free-form treatment is enabled.
     *     <li>Activity isn't opted out by the device manufacturer with override.
     * </ul>
     */
    @VisibleForTesting
    boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) {
        return Flags.cameraCompatForFreeform() && !activity.info.isChangeEnabled(
                ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
    }

    @Override
    public boolean onCameraOpened(@NonNull ActivityRecord cameraActivity,
            @NonNull String cameraId) {
        if (!isTreatmentEnabledForActivity(cameraActivity)) {
            return false;
        }
        final int existingCameraCompatMode =
                cameraActivity.mLetterboxUiController.getFreeformCameraCompatMode();
        final int newCameraCompatMode = getCameraCompatMode(cameraActivity);
        if (newCameraCompatMode != existingCameraCompatMode) {
            mIsCameraCompatTreatmentPending = true;
            cameraActivity.mLetterboxUiController.setFreeformCameraCompatMode(newCameraCompatMode);
            forceUpdateActivityAndTask(cameraActivity);
            return true;
        } else {
            mIsCameraCompatTreatmentPending = false;
        }
        return false;
    }

    @Override
    public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity,
            @NonNull String cameraId) {
        if (isActivityForCameraIdRefreshing(cameraId)) {
            ProtoLog.v(ProtoLogGroup.WM_DEBUG_STATES,
                    "Display id=%d is notified that Camera %s is closed but activity is"
                            + " still refreshing. Rescheduling an update.",
                    mDisplayContent.mDisplayId, cameraId);
            return false;
        }
        cameraActivity.mLetterboxUiController.setFreeformCameraCompatMode(
                CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE);
        forceUpdateActivityAndTask(cameraActivity);
        mIsCameraCompatTreatmentPending = false;
        return true;
    }

    private void forceUpdateActivityAndTask(ActivityRecord cameraActivity) {
        cameraActivity.recomputeConfiguration();
        cameraActivity.updateReportedConfigurationAndSend();
        Task cameraTask = cameraActivity.getTask();
        if (cameraTask != null) {
            cameraTask.dispatchTaskInfoChangedIfNeeded(/* force= */ true);
        }
    }

    private static int getCameraCompatMode(@NonNull ActivityRecord topActivity) {
        return switch (topActivity.getRequestedConfigurationOrientation()) {
            case ORIENTATION_PORTRAIT -> CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT;
            case ORIENTATION_LANDSCAPE -> CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_LANDSCAPE;
            default -> CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
        };
    }

    /**
     * Whether camera compat treatment is applicable for the given activity, ignoring its windowing
     * mode.
     *
     * <p>Conditions that need to be met:
     * <ul>
     *     <li>Treatment is enabled.
     *     <li>Camera is active for the package.
     *     <li>The app has a fixed orientation.
     *     <li>The app is in freeform windowing mode.
     * </ul>
     */
    private boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) {
        int orientation = activity.getRequestedConfigurationOrientation();
        return shouldApplyFreeformTreatmentForCameraCompat(activity)
                && mCameraStateMonitor.isCameraRunningForActivity(activity)
                && 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.
                && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR
                && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED
                // TODO(b/332665280): investigate whether we can support activity embedding.
                && !activity.isEmbedded();
    }

    private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) {
        final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
                /* considerKeyguardState= */ true);
        if (topActivity == null || !isTreatmentEnabledForActivity(topActivity)
                || mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
            return false;
        }
        return topActivity.mLetterboxUiController.isRefreshRequested();
    }
}
+27 −5
Original line number Diff line number Diff line
@@ -263,6 +263,7 @@ import com.android.server.policy.WindowManagerPolicy;
import com.android.server.wm.utils.RegionUtils;
import com.android.server.wm.utils.RotationCache;
import com.android.server.wm.utils.WmDisplayCutout;
import com.android.window.flags.Flags;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -477,6 +478,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
    @Nullable
    final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
    @Nullable
    final CameraCompatFreeformPolicy mCameraCompatFreeformPolicy;
    @Nullable
    final CameraStateMonitor mCameraStateMonitor;
    @Nullable
    final ActivityRefresher mActivityRefresher;
@@ -683,7 +686,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
     */
    private InputTarget mLastImeInputTarget;


    /**
     * Tracks the windowToken of the input method input target and the corresponding
     * {@link WindowContainerListener} for monitoring changes (e.g. the requested visibility
@@ -1233,11 +1235,26 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
        // without the need to restart the device.
        final boolean shouldCreateDisplayRotationCompatPolicy =
                mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
        if (shouldCreateDisplayRotationCompatPolicy) {
        final boolean shouldCreateCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform()
                && DesktopModeLaunchParamsModifier.canEnterDesktopMode(mWmService.mContext);
        if (shouldCreateDisplayRotationCompatPolicy || shouldCreateCameraCompatFreeformPolicy) {
            mCameraStateMonitor = new CameraStateMonitor(this, mWmService.mH);
            mActivityRefresher = new ActivityRefresher(mWmService, mWmService.mH);
            mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(
                    this, mCameraStateMonitor, mActivityRefresher);
            if (shouldCreateDisplayRotationCompatPolicy) {
                mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(this,
                        mCameraStateMonitor, mActivityRefresher);
                mDisplayRotationCompatPolicy.start();
            } else {
                mDisplayRotationCompatPolicy = null;
            }

            if (shouldCreateCameraCompatFreeformPolicy) {
                mCameraCompatFreeformPolicy = new CameraCompatFreeformPolicy(this,
                        mCameraStateMonitor, mActivityRefresher);
                mCameraCompatFreeformPolicy.start();
            } else {
                mCameraCompatFreeformPolicy = null;
            }

            mCameraStateMonitor.startListeningToCameraState();
        } else {
@@ -1245,9 +1262,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
            mCameraStateMonitor = null;
            mActivityRefresher = null;
            mDisplayRotationCompatPolicy = null;
            mCameraCompatFreeformPolicy = null;
        }


        mRotationReversionController = new DisplayRotationReversionController(this);

        mInputMonitor = new InputMonitor(mWmService, this);
@@ -3350,6 +3367,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
        if (mDisplayRotationCompatPolicy != null) {
            mDisplayRotationCompatPolicy.dispose();
        }

        if (mCameraCompatFreeformPolicy != null) {
            mCameraCompatFreeformPolicy.dispose();
        }

        if (mCameraStateMonitor != null) {
            mCameraStateMonitor.dispose();
        }
+5 −2
Original line number Diff line number Diff line
@@ -80,8 +80,11 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp
        mDisplayContent = displayContent;
        mWmService = displayContent.mWmService;
        mCameraStateMonitor = cameraStateMonitor;
        mCameraStateMonitor.addCameraStateListener(this);
        mActivityRefresher = activityRefresher;
    }

    void start() {
        mCameraStateMonitor.addCameraStateListener(this);
        mActivityRefresher.addEvaluator(this);
    }

@@ -365,7 +368,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp
    }

    // TODO(b/336474959): Do we need cameraId here?
    private boolean isActivityForCameraIdRefreshing(String cameraId) {
    private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) {
        final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
                /* considerKeyguardState= */ true);
        if (!isTreatmentEnabledForActivity(topActivity)
+14 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.wm;

import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
@@ -103,6 +104,7 @@ import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTy
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager.TaskDescription;
import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
@@ -231,6 +233,9 @@ final class LetterboxUiController {

    private boolean mDoubleTapEvent;

    @FreeformCameraCompatMode
    private int mFreeformCameraCompatMode = CAMERA_COMPAT_FREEFORM_NONE;

    LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
        mLetterboxConfiguration = wmService.mLetterboxConfiguration;
        // Given activityRecord may not be fully constructed since LetterboxUiController
@@ -711,6 +716,15 @@ final class LetterboxUiController {
                        .isTreatmentEnabledForActivity(mActivityRecord);
    }

    @FreeformCameraCompatMode
    int getFreeformCameraCompatMode() {
        return mFreeformCameraCompatMode;
    }

    void setFreeformCameraCompatMode(@FreeformCameraCompatMode int freeformCameraCompatMode) {
        mFreeformCameraCompatMode = freeformCameraCompatMode;
    }

    private boolean isCompatChangeEnabled(long overrideChangeId) {
        return mActivityRecord.info.isChangeEnabled(overrideChangeId);
    }
Loading