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

Commit 0f1a3477 authored by Kazuki Takise's avatar Kazuki Takise Committed by Android (Google) Code Review
Browse files

Merge "Implement display compat mode" into main

parents 6b81bf05 a0303b71
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -1721,7 +1721,7 @@ final class ActivityRecord extends WindowToken {
        }

        mAppCompatController.getLetterboxPolicy().onMovedToDisplay(mDisplayContent.getDisplayId());
        mAppCompatController.getDisplayCompatModePolicy().onMovedToDisplay();
        mAppCompatController.getDisplayCompatModePolicy().onMovedToDisplay(prevDc, dc);
    }

    void layoutLetterboxIfNeeded(WindowState winHint) {
@@ -8590,6 +8590,10 @@ final class ActivityRecord extends WindowToken {
            configChanged |= CONFIG_UI_MODE;
        }

        // Some apps relaunch unexpectedly with display move and crash.
        configChanged |= mAppCompatController.getDisplayCompatModePolicy()
                .getDisplayCompatModeConfigMask();

        return (changes & (~configChanged)) != 0;
    }

+1 −1
Original line number Diff line number Diff line
@@ -74,7 +74,7 @@ class AppCompatController {
        mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord,
                mAppCompatOverrides);
        mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord);
        mDisplayCompatModePolicy = new AppCompatDisplayCompatModePolicy();
        mDisplayCompatModePolicy = new AppCompatDisplayCompatModePolicy(activityRecord);
    }

    @NonNull
+89 −5
Original line number Diff line number Diff line
@@ -16,24 +16,108 @@

package com.android.server.wm;

import static android.content.pm.ActivityInfo.CONFIG_COLOR_MODE;
import static android.content.pm.ActivityInfo.CONFIG_DENSITY;
import static android.content.pm.ActivityInfo.CONFIG_TOUCHSCREEN;
import static android.view.Display.TYPE_INTERNAL;

import android.annotation.NonNull;
import android.content.pm.ApplicationInfo;

import com.android.window.flags.Flags;

/**
 * Encapsulate app-compat logic for multi-display environments.
 *
 * <p>The primary feature is "display compat mode", which suppresses automatic activity restart
 * caused by display-specific config changes to prevent unexpected crashes.
 * The current conditions of an app being in display compat mode are:
 * <ul>
 *     <li>The app is a game.
 *     <li>The app doesn't support all of {@link DISPLAY_COMPAT_MODE_CONFIG_MASK}.
 *     <li>The app has moved to a different display but not restarted yet.
 * </ul>
 *
 * <p>Display compat mode comes with restart handle menu, with which the app process gets recreated,
 * and all the config changes get reloaded by the app, at the timing the user wants to do so with
 * the risk of losing the current state of the app.
 */
class AppCompatDisplayCompatModePolicy {

    private boolean mIsRestartMenuEnabledForDisplayMove;
    private static final int DISPLAY_COMPAT_MODE_CONFIG_MASK =
            CONFIG_DENSITY | CONFIG_TOUCHSCREEN | CONFIG_COLOR_MODE;

    @NonNull
    private final ActivityRecord mActivityRecord;

    private boolean mDisplayChangedWithoutRestart;

    AppCompatDisplayCompatModePolicy(@NonNull ActivityRecord activityRecord) {
        mActivityRecord = activityRecord;
    }

    /**
     * Returns whether the restart menu is enabled for display move. Currently it only gets shown
     * when an app is in display compat mode.
     *
     * @return {@code true} if the restart menu should be enabled for display move.
     */
    boolean isRestartMenuEnabledForDisplayMove() {
        return Flags.enableRestartMenuForConnectedDisplays() && mIsRestartMenuEnabledForDisplayMove;
        // Restart menu is only available to apps in display compat mode.
        return Flags.enableRestartMenuForConnectedDisplays() && mDisplayChangedWithoutRestart
                && getDisplayCompatModeConfigMask() != 0;
    }

    void onMovedToDisplay() {
        mIsRestartMenuEnabledForDisplayMove = true;
    /**
     * Called when the activity is moved to a different display.
     *
     * @param previousDisplay The display the app was on before this display transition.
     * @param newDisplay The new display the app got moved onto.
     */
    void onMovedToDisplay(@NonNull DisplayContent previousDisplay,
            @NonNull DisplayContent newDisplay) {
        if (previousDisplay.getDisplayInfo().type == TYPE_INTERNAL
                && newDisplay.getDisplayInfo().type == TYPE_INTERNAL) {
            // A transition between internal displays (fold<->unfold on foldable) is not considered
            // display move here for now because they generally have many configurations in common,
            // thus are less likely to cause compat issues.
            return;
        }
        mDisplayChangedWithoutRestart = true;
    }

    /**
     * Called when the activity's process is restarted.
     */
    void onProcessRestarted() {
        mIsRestartMenuEnabledForDisplayMove = false;
        mDisplayChangedWithoutRestart = false;
    }

    /**
     * Returns the mask of the config changes that should not trigger activity restart with display
     * move for app-compat reasons.
     *
     * @return the mask of the config changes that should not trigger activity restart or 0 if
     * display compat mode is not enabled for the activity.
     */
    int getDisplayCompatModeConfigMask() {
        if (!Flags.enableDisplayCompatMode()) return 0;

        if (mActivityRecord.info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
            // A large majority of apps that crash with display move are games. Apply this compat
            // treatment only to games to minimize risk.
            return 0;
        }

        if (!mDisplayChangedWithoutRestart) {
            // Enable display compat mode when display move is involved.
            return 0;
        }

        // If a specific config change is supported by the activity, it's exempted from this compat
        // treatment. This way, apps can opt out from display compat mode by handling all the config
        // changes that happen with display move by themselves.
        final int supportedConfigChanged = mActivityRecord.info.getRealConfigChanged();
        return DISPLAY_COMPAT_MODE_CONFIG_MASK & (~supportedConfigChanged);
    }
}
+54 −1
Original line number Diff line number Diff line
@@ -22,13 +22,18 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.content.pm.ApplicationInfo.CATEGORY_GAME;
import static android.content.pm.ApplicationInfo.CATEGORY_UNDEFINED;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.wm.ActivityRecord.State.RESUMED;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.WindowConfiguration.WindowingMode;
@@ -37,6 +42,9 @@ import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager.UserMinAspectRatio;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.InputDevice;
import android.view.Surface;

import androidx.annotation.CallSuper;
@@ -188,6 +196,7 @@ class AppCompatActivityRobot {
    void moveTaskToSecondaryDisplay() {
        mTaskStack.top().reparent(mSecondaryDisplayContent.getDefaultTaskDisplayArea(),
                true /* onTop */);
        mActivityStack.top().ensureActivityConfiguration();
    }

    void setTaskDisplayAreaWindowingMode(@WindowingMode int windowingMode) {
@@ -313,6 +322,21 @@ class AppCompatActivityRobot {
        }
    }

    void setTopActivityGame(boolean isGame) {
        mActivityStack.top().info.applicationInfo.category =
                isGame ? CATEGORY_GAME : CATEGORY_UNDEFINED;
    }

    void setTopActivityResumed() {
        mActivityStack.top().setVisible(true);
        mActivityStack.top().setVisibleRequested(true);
        mActivityStack.top().setState(RESUMED, "setTopActivityResumed");
    }

    void setTopActivityConfigChanges(int supportedConfigChanges) {
        mActivityStack.top().info.configChanges = supportedConfigChanges;
    }

    void setFixedRotationTransformDisplayBounds(@Nullable Rect bounds) {
        doReturn(bounds).when(mActivityStack.top()).getFixedRotationTransformDisplayBounds();
    }
@@ -331,9 +355,33 @@ class AppCompatActivityRobot {
        onPostDisplayContentCreation(mDisplayContent);
    }

    /**
     * Creates a secondary display. Its density, color mode, and touchscreen are intentionally set
     * different from those of the default display to emulate common physical environments.
     */
    void createSecondaryDisplay() {
        final DisplayInfo displayInfo = new DisplayInfo();
        displayInfo.copyFrom(mDisplayContent.getDisplayInfo());
        displayInfo.type = Display.TYPE_EXTERNAL;
        final int[] hdrTypesWithDv = new int[] {1, 2, 3, 4};
        displayInfo.hdrCapabilities = new Display.HdrCapabilities(hdrTypesWithDv, 0, 0, 0);
        doReturn(true).when(mAtm.mWindowManager).hasHdrSupport();

        mSecondaryDisplayContent =
                new TestDisplayContent.Builder(mAtm, mDisplayWidth, mDisplayHeight).build();
                new TestDisplayContent.Builder(mAtm, displayInfo).build();

        mSecondaryDisplayContent.setForcedDensity(123, mAtm.mWindowManager.mCurrentUserId);

        final InputDevice device = new InputDevice.Builder()
                .setAssociatedDisplayId(mSecondaryDisplayContent.mDisplayId)
                .setSources(InputDevice.SOURCE_TOUCHSCREEN)
                .build();
        final InputDevice[] devices = {device};
        doReturn(true).when(mSecondaryDisplayContent.mWmService.mInputManager)
                .canDispatchToDisplay(device.getId(), mSecondaryDisplayContent.mDisplayId);
        doReturn(devices).when(mSecondaryDisplayContent.mWmService.mInputManager).getInputDevices();
        mAtm.mWindowManager.mIsTouchDevice = true;
        mAtm.mWindowManager.displayReady();

        onPostDisplayContentCreation(mSecondaryDisplayContent);
    }
@@ -421,6 +469,11 @@ class AppCompatActivityRobot {
        Assert.assertNull(getter.apply(mActivityStack.top()));
    }

    void checkTopActivityRelaunched(boolean relaunched) {
        verify(mActivityStack.top(), times(relaunched ? 1 : 0)).relaunchActivityLocked(anyBoolean(),
                anyInt());
    }

    void checkTopActivityRecomputedConfiguration() {
        verify(mActivityStack.top()).recomputeConfiguration();
    }
+30 −2
Original line number Diff line number Diff line
@@ -16,6 +16,12 @@

package com.android.server.wm;

import static android.content.pm.ActivityInfo.CONFIG_COLOR_MODE;
import static android.content.pm.ActivityInfo.CONFIG_DENSITY;
import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED;
import static android.content.pm.ActivityInfo.CONFIG_TOUCHSCREEN;

import static com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_COMPAT_MODE;
import static com.android.window.flags.Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS;

import static org.junit.Assert.assertEquals;
@@ -42,15 +48,22 @@ import java.util.function.Consumer;
@RunWith(WindowTestRunner.class)
public class AppCompatDisplayCompatTests extends WindowTestsBase {

    @EnableFlags(FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS)
    private static final int CONFIG_MASK_FOR_DISPLAY_MOVE =
            ~(CONFIG_DENSITY | CONFIG_TOUCHSCREEN | CONFIG_COLOR_MODE | CONFIG_RESOURCES_UNUSED);

    @EnableFlags({FLAG_ENABLE_DISPLAY_COMPAT_MODE, FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS})
    @Test
    public void testRestartMenuVisibility() {
    public void testDisplayCompatMode_gameDoesNotRestartWithDisplayMove() {
        runTestScenario((robot) -> {
            robot.activity().createSecondaryDisplay();
            robot.activity().createActivityWithComponent();
            robot.activity().setTopActivityGame(true);
            robot.activity().setTopActivityResumed();
            robot.activity().setTopActivityConfigChanges(CONFIG_MASK_FOR_DISPLAY_MOVE);
            robot.checkRestartMenuVisibility(false);

            robot.activity().moveTaskToSecondaryDisplay();
            robot.activity().checkTopActivityRelaunched(false);
            robot.checkRestartMenuVisibility(true);

            robot.activity().applyToTopActivity(ActivityRecord::restartProcessIfVisible);
@@ -58,6 +71,21 @@ public class AppCompatDisplayCompatTests extends WindowTestsBase {
        });
    }

    @Test
    public void testDisplayCompatMode_nonGameRestartsWithDisplayMove() {
        runTestScenario((robot) -> {
            robot.activity().createSecondaryDisplay();
            robot.activity().createActivityWithComponent();
            robot.activity().setTopActivityResumed();
            robot.activity().setTopActivityConfigChanges(CONFIG_MASK_FOR_DISPLAY_MOVE);
            robot.checkRestartMenuVisibility(false);

            robot.activity().moveTaskToSecondaryDisplay();
            robot.activity().checkTopActivityRelaunched(true);
            robot.checkRestartMenuVisibility(false);
        });
    }

    void runTestScenario(@NonNull Consumer<DisplayCompatRobotTest> consumer) {
        final DisplayCompatRobotTest robot = new DisplayCompatRobotTest(mWm, mAtm, mSupervisor);
        consumer.accept(robot);