Loading core/java/android/content/pm/ActivityInfo.java +16 −0 Original line number Diff line number Diff line Loading @@ -1714,6 +1714,22 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @Disabled public static final long OVERRIDE_MOUSE_TO_TOUCH = 413207127L; /** * This change id automatically restarts apps when they move between displays. * * <p>Some apps don't work well with density change. The override enabled by this change id * allows them to automatically restart their process to ensure that UI is rendered based on the * correct density. This is disabled by default, and can be enabled by device manufacturers on a * per-application basis, controlled via * <a href="https://developer.android.com/guide/practices/device-compatibility-mode#device_manufacturer_per-app_overrides">Device manufacturer per-app overrides</a>. * * @hide */ @ChangeId @Overridable @Disabled public static final long OVERRIDE_AUTO_RESTART_ON_DISPLAY_MOVE = 427878712L; /** * Optional set of a certificates identifying apps that are allowed to embed this activity. From * the "knownActivityEmbeddingCerts" attribute. Loading services/core/java/com/android/server/wm/ActivityRecord.java +51 −26 Original line number Diff line number Diff line Loading @@ -143,6 +143,7 @@ import static android.view.WindowManager.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENAB import static android.view.WindowManager.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.hasWindowExtensionsEnabled; import static android.window.DesktopExperienceFlags.ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE; import static android.window.DesktopExperienceFlags.ENABLE_DRAGGING_PIP_ACROSS_DISPLAYS; import static android.window.DesktopExperienceFlags.ENABLE_PIP_PARAMS_UPDATE_NOTIFICATION_BUGFIX; import static android.window.DesktopExperienceFlags.ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS; Loading Loading @@ -8778,8 +8779,6 @@ final class ActivityRecord extends WindowToken { // configuration. mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); mAppCompatController.getDisplayCompatModePolicy().onProcessRestarted(); final boolean fullscreenOverrideChanged = mAppCompatController.getAspectRatioOverrides().resetSystemFullscreenOverrideCache(); if (!attachedToProcess()) { return; Loading @@ -8788,7 +8787,52 @@ final class ActivityRecord extends WindowToken { // The restarting state avoids removing this record when process is died. setState(RESTARTING_PROCESS, "restartActivityProcess"); if (!mVisibleRequested || mHaveState) { if (mTransitionController.isShellTransitionsEnabled()) { if (!ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && killInvisibleProcessOrPrepareForRestart()) { return; } final Transition transition = new Transition(TRANSIT_RELAUNCH, 0 /* flags */, mTransitionController, mWmService.mSyncEngine); mTransitionController.startCollectOrQueue(transition, (deferred) -> { if (ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && killInvisibleProcessOrPrepareForRestart()) { transition.abort(); return; } if (!ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && mState != RESTARTING_PROCESS) { transition.abort(); return; } if (!attachedToProcess()) { transition.abort(); return; } final ActionChain chain = mAtmService.mChainTracker.start( "restartProc", transition); chain.collect(this); // Make sure this will be a change in the transition. transition.setKnownConfigChanges(this, CONFIG_WINDOW_CONFIGURATION); mTransitionController.requestStartTransition(transition, task, null /* remoteTransition */, null /* displayChange */); scheduleStopForRestartProcess(); mAtmService.mChainTracker.end(); }); } else { if (killInvisibleProcessOrPrepareForRestart()) { return; } scheduleStopForRestartProcess(); } } /** * Returns {@code true} if the process is killed as the app is invisible. Otherwise, do some * preparation to restart the process. */ private boolean killInvisibleProcessOrPrepareForRestart() { if (!mVisibleRequested || (!ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && mHaveState)) { // Kill its process immediately because the activity should be in background. // The activity state will be update to {@link #DESTROYED} in // {@link ActivityStack#cleanUp} when handling process died. Loading @@ -8803,9 +8847,11 @@ final class ActivityRecord extends WindowToken { } mAtmService.mAmInternal.killProcess(wpc.mName, wpc.mUid, "resetConfig"); }); return; return true; } final boolean fullscreenOverrideChanged = mAppCompatController.getAspectRatioOverrides().resetSystemFullscreenOverrideCache(); if (fullscreenOverrideChanged) { task.updateForceResizeOverridesIfNeeded(this); } Loading @@ -8817,28 +8863,7 @@ final class ActivityRecord extends WindowToken { if (ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS.isTrue()) { mAtmService.resumeAppSwitches(); } if (mTransitionController.isShellTransitionsEnabled()) { final Transition transition = new Transition(TRANSIT_RELAUNCH, 0 /* flags */, mTransitionController, mWmService.mSyncEngine); mTransitionController.startCollectOrQueue(transition, (deferred) -> { if (mState != RESTARTING_PROCESS || !attachedToProcess()) { transition.abort(); return; } final ActionChain chain = mAtmService.mChainTracker.start( "restartProc", transition); chain.collect(this); // Make sure this will be a change in the transition. transition.setKnownConfigChanges(this, CONFIG_WINDOW_CONFIGURATION); mTransitionController.requestStartTransition(transition, task, null /* remoteTransition */, null /* displayChange */); scheduleStopForRestartProcess(); mAtmService.mChainTracker.end(); }); } else { scheduleStopForRestartProcess(); } return false; } private void scheduleStopForRestartProcess() { Loading services/core/java/com/android/server/wm/AppCompatController.java +5 −0 Original line number Diff line number Diff line Loading @@ -168,6 +168,11 @@ class AppCompatController { return mDisplayCompatModePolicy; } @NonNull AppCompatDisplayOverrides getDisplayOverrides() { return mAppCompatOverrides.getDisplayOverrides(); } void dump(@NonNull PrintWriter pw, @NonNull String prefix) { getTransparentPolicy().dump(pw, prefix); getLetterboxPolicy().dump(pw, prefix); Loading services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java +18 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ 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 static android.window.DesktopExperienceFlags.ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE; import static android.window.DesktopExperienceFlags.ENABLE_DISPLAY_COMPAT_MODE; import static android.window.DesktopExperienceFlags.ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS; Loading Loading @@ -71,6 +72,15 @@ class AppCompatDisplayCompatModePolicy { || (mActivityRecord.inSizeCompatMode() && mDisplayChangedWithoutRestart)); } /** * Returns whether the app should be restarted when moved to a different display for app-compat. */ boolean shouldRestartOnDisplayMove() { // TODO(b/427878712): Discuss opt-in/out policies. return mActivityRecord.mAppCompatController.getDisplayOverrides() .shouldRestartOnDisplayMove(); } /** * Called when the activity is moved to a different display. * Loading @@ -87,6 +97,14 @@ class AppCompatDisplayCompatModePolicy { return; } mDisplayChangedWithoutRestart = true; if (ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && shouldRestartOnDisplayMove()) { // At this point, a transition for moving the app between displays should be running, so // the restarting logic below will be queued as a new transition, which means the // configuration change for the display move has been processed when the process is // restarted. This allows the app to be launched in the latest configuration. mActivityRecord.restartProcessIfVisible(); } } /** Loading services/core/java/com/android/server/wm/AppCompatDisplayOverrides.java 0 → 100644 +43 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.OVERRIDE_AUTO_RESTART_ON_DISPLAY_MOVE; import static com.android.server.wm.AppCompatUtils.isChangeEnabled; import android.annotation.NonNull; /** * Encapsulates app compat configurations and overrides related to display. */ class AppCompatDisplayOverrides { @NonNull private final ActivityRecord mActivityRecord; AppCompatDisplayOverrides(@NonNull ActivityRecord activityRecord) { mActivityRecord = activityRecord; } /** * Whether the activity should be restarted when moved to a different display. */ boolean shouldRestartOnDisplayMove() { return isChangeEnabled(mActivityRecord, OVERRIDE_AUTO_RESTART_ON_DISPLAY_MOVE); } } Loading
core/java/android/content/pm/ActivityInfo.java +16 −0 Original line number Diff line number Diff line Loading @@ -1714,6 +1714,22 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @Disabled public static final long OVERRIDE_MOUSE_TO_TOUCH = 413207127L; /** * This change id automatically restarts apps when they move between displays. * * <p>Some apps don't work well with density change. The override enabled by this change id * allows them to automatically restart their process to ensure that UI is rendered based on the * correct density. This is disabled by default, and can be enabled by device manufacturers on a * per-application basis, controlled via * <a href="https://developer.android.com/guide/practices/device-compatibility-mode#device_manufacturer_per-app_overrides">Device manufacturer per-app overrides</a>. * * @hide */ @ChangeId @Overridable @Disabled public static final long OVERRIDE_AUTO_RESTART_ON_DISPLAY_MOVE = 427878712L; /** * Optional set of a certificates identifying apps that are allowed to embed this activity. From * the "knownActivityEmbeddingCerts" attribute. Loading
services/core/java/com/android/server/wm/ActivityRecord.java +51 −26 Original line number Diff line number Diff line Loading @@ -143,6 +143,7 @@ import static android.view.WindowManager.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENAB import static android.view.WindowManager.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.hasWindowExtensionsEnabled; import static android.window.DesktopExperienceFlags.ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE; import static android.window.DesktopExperienceFlags.ENABLE_DRAGGING_PIP_ACROSS_DISPLAYS; import static android.window.DesktopExperienceFlags.ENABLE_PIP_PARAMS_UPDATE_NOTIFICATION_BUGFIX; import static android.window.DesktopExperienceFlags.ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS; Loading Loading @@ -8778,8 +8779,6 @@ final class ActivityRecord extends WindowToken { // configuration. mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); mAppCompatController.getDisplayCompatModePolicy().onProcessRestarted(); final boolean fullscreenOverrideChanged = mAppCompatController.getAspectRatioOverrides().resetSystemFullscreenOverrideCache(); if (!attachedToProcess()) { return; Loading @@ -8788,7 +8787,52 @@ final class ActivityRecord extends WindowToken { // The restarting state avoids removing this record when process is died. setState(RESTARTING_PROCESS, "restartActivityProcess"); if (!mVisibleRequested || mHaveState) { if (mTransitionController.isShellTransitionsEnabled()) { if (!ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && killInvisibleProcessOrPrepareForRestart()) { return; } final Transition transition = new Transition(TRANSIT_RELAUNCH, 0 /* flags */, mTransitionController, mWmService.mSyncEngine); mTransitionController.startCollectOrQueue(transition, (deferred) -> { if (ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && killInvisibleProcessOrPrepareForRestart()) { transition.abort(); return; } if (!ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && mState != RESTARTING_PROCESS) { transition.abort(); return; } if (!attachedToProcess()) { transition.abort(); return; } final ActionChain chain = mAtmService.mChainTracker.start( "restartProc", transition); chain.collect(this); // Make sure this will be a change in the transition. transition.setKnownConfigChanges(this, CONFIG_WINDOW_CONFIGURATION); mTransitionController.requestStartTransition(transition, task, null /* remoteTransition */, null /* displayChange */); scheduleStopForRestartProcess(); mAtmService.mChainTracker.end(); }); } else { if (killInvisibleProcessOrPrepareForRestart()) { return; } scheduleStopForRestartProcess(); } } /** * Returns {@code true} if the process is killed as the app is invisible. Otherwise, do some * preparation to restart the process. */ private boolean killInvisibleProcessOrPrepareForRestart() { if (!mVisibleRequested || (!ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && mHaveState)) { // Kill its process immediately because the activity should be in background. // The activity state will be update to {@link #DESTROYED} in // {@link ActivityStack#cleanUp} when handling process died. Loading @@ -8803,9 +8847,11 @@ final class ActivityRecord extends WindowToken { } mAtmService.mAmInternal.killProcess(wpc.mName, wpc.mUid, "resetConfig"); }); return; return true; } final boolean fullscreenOverrideChanged = mAppCompatController.getAspectRatioOverrides().resetSystemFullscreenOverrideCache(); if (fullscreenOverrideChanged) { task.updateForceResizeOverridesIfNeeded(this); } Loading @@ -8817,28 +8863,7 @@ final class ActivityRecord extends WindowToken { if (ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS.isTrue()) { mAtmService.resumeAppSwitches(); } if (mTransitionController.isShellTransitionsEnabled()) { final Transition transition = new Transition(TRANSIT_RELAUNCH, 0 /* flags */, mTransitionController, mWmService.mSyncEngine); mTransitionController.startCollectOrQueue(transition, (deferred) -> { if (mState != RESTARTING_PROCESS || !attachedToProcess()) { transition.abort(); return; } final ActionChain chain = mAtmService.mChainTracker.start( "restartProc", transition); chain.collect(this); // Make sure this will be a change in the transition. transition.setKnownConfigChanges(this, CONFIG_WINDOW_CONFIGURATION); mTransitionController.requestStartTransition(transition, task, null /* remoteTransition */, null /* displayChange */); scheduleStopForRestartProcess(); mAtmService.mChainTracker.end(); }); } else { scheduleStopForRestartProcess(); } return false; } private void scheduleStopForRestartProcess() { Loading
services/core/java/com/android/server/wm/AppCompatController.java +5 −0 Original line number Diff line number Diff line Loading @@ -168,6 +168,11 @@ class AppCompatController { return mDisplayCompatModePolicy; } @NonNull AppCompatDisplayOverrides getDisplayOverrides() { return mAppCompatOverrides.getDisplayOverrides(); } void dump(@NonNull PrintWriter pw, @NonNull String prefix) { getTransparentPolicy().dump(pw, prefix); getLetterboxPolicy().dump(pw, prefix); Loading
services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java +18 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ 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 static android.window.DesktopExperienceFlags.ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE; import static android.window.DesktopExperienceFlags.ENABLE_DISPLAY_COMPAT_MODE; import static android.window.DesktopExperienceFlags.ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS; Loading Loading @@ -71,6 +72,15 @@ class AppCompatDisplayCompatModePolicy { || (mActivityRecord.inSizeCompatMode() && mDisplayChangedWithoutRestart)); } /** * Returns whether the app should be restarted when moved to a different display for app-compat. */ boolean shouldRestartOnDisplayMove() { // TODO(b/427878712): Discuss opt-in/out policies. return mActivityRecord.mAppCompatController.getDisplayOverrides() .shouldRestartOnDisplayMove(); } /** * Called when the activity is moved to a different display. * Loading @@ -87,6 +97,14 @@ class AppCompatDisplayCompatModePolicy { return; } mDisplayChangedWithoutRestart = true; if (ENABLE_AUTO_RESTART_ON_DISPLAY_MOVE.isTrue() && shouldRestartOnDisplayMove()) { // At this point, a transition for moving the app between displays should be running, so // the restarting logic below will be queued as a new transition, which means the // configuration change for the display move has been processed when the process is // restarted. This allows the app to be launched in the latest configuration. mActivityRecord.restartProcessIfVisible(); } } /** Loading
services/core/java/com/android/server/wm/AppCompatDisplayOverrides.java 0 → 100644 +43 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.OVERRIDE_AUTO_RESTART_ON_DISPLAY_MOVE; import static com.android.server.wm.AppCompatUtils.isChangeEnabled; import android.annotation.NonNull; /** * Encapsulates app compat configurations and overrides related to display. */ class AppCompatDisplayOverrides { @NonNull private final ActivityRecord mActivityRecord; AppCompatDisplayOverrides(@NonNull ActivityRecord activityRecord) { mActivityRecord = activityRecord; } /** * Whether the activity should be restarted when moved to a different display. */ boolean shouldRestartOnDisplayMove() { return isChangeEnabled(mActivityRecord, OVERRIDE_AUTO_RESTART_ON_DISPLAY_MOVE); } }