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

Commit 1b3ce29b authored by Kazuki Takise's avatar Kazuki Takise
Browse files

Reland: Implement auto-restart on display move

Some  apps cache density in a place independent from
activity lifecycle, so when they move between displays, it's not
refreshed to the latest value, which makes the UI too big/small
on the new display.

To fix this, this change introduces a new per-app override that
automatically restarts apps when they move between displays.

The existing restartProcessIfVisible() doesn't work as is in this
new scenario as in our case processes are restarted during
display move, where a lot of other changes happen such as config
changes and lifecycle events, so this change makes some
improvements in the function:
- Lifecycle events are async, so the checks of mState and
 mHaveState are removed.
- Visibility can be toggled while the transition for restart is
 in the transition queue, so the visibility check to
 startCollectOrQueue() is moved.

Flag: com.android.window.flags.enable_auto_restart_on_display_move
Bug: 427878712
Test: AppCompatDisplayCompatTests
Test: AppCompatDisplayOverridesTest
Change-Id: I09c2a8df6c05f11a980fd21b8c603b7839b19833
parent 60499efc
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -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.
+51 −26
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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.
@@ -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);
        }
@@ -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() {
+5 −0
Original line number Diff line number Diff line
@@ -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);
+18 −0
Original line number Diff line number Diff line
@@ -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;

@@ -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.
     *
@@ -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();
        }
    }

    /**
+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