Turn OFF any changing displays during device-state transitions

When transitioning from one device state to another,
turn off any displays that are going to change. This avoids
some jank and allows for devices that don't want more than
one display on at once.

1) Changes the isEnabled variable of LogicalDisplay to
a concept of display phases which can go between DISABLED,
ON, and TRANSITION. DISABLED meants the display is not visible to the
rest of the system. TRANSITION meants the display should remain off.
ON means that the display is running normally and its power should be
calculated by DisplayPowerController.

2) Displays will now remain off until WindowManager is finished
rendering the windows, which removes the device-state-transition
window jank.

Test: atest com.android.server.display
Bug: 183001297
Bug: 183895829
Bug: 175806738
Bug: 185354618
Change-Id: Icae68121162ff72913ff72ea73261ec0f448f330
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 529aa66..48f4d28 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -657,6 +657,12 @@
     <!-- Indicate the display area rect for foldable devices in folded state. -->
     <string name="config_foldedArea"></string>
 
+    <!-- Indicates that the device supports having more than one internal display on at the same
+         time. Only applicable to devices with more than one internal display. If this option is
+         set to false, DisplayManager will make additional effort to ensure no more than 1 internal
+         display is powered on at the same time. -->
+    <bool name="config_supportsConcurrentInternalDisplays">true</bool>
+
     <!-- Desk dock behavior -->
 
     <!-- The number of degrees to rotate the display when the device is in a desk dock.
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 78a794a..9ac9e8e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3800,6 +3800,7 @@
   <!-- For Foldables -->
   <java-symbol type="array" name="config_foldedDeviceStates" />
   <java-symbol type="string" name="config_foldedArea" />
+  <java-symbol type="bool" name="config_supportsConcurrentInternalDisplays" />
 
   <java-symbol type="array" name="config_disableApksUnlessMatchedSku_apk_list" />
   <java-symbol type="array" name="config_disableApkUnlessMatchedSku_skus_list" />
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 393a4eb..96641a6 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -430,8 +430,8 @@
         mHandler = new DisplayManagerHandler(DisplayThread.get().getLooper());
         mUiHandler = UiThread.getHandler();
         mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore);
-        mLogicalDisplayMapper = new LogicalDisplayMapper(mDisplayDeviceRepo,
-                new LogicalDisplayListener());
+        mLogicalDisplayMapper = new LogicalDisplayMapper(mContext, mDisplayDeviceRepo,
+                new LogicalDisplayListener(), mSyncRoot, mHandler);
         mDisplayModeDirector = new DisplayModeDirector(context, mHandler);
         mBrightnessSynchronizer = new BrightnessSynchronizer(mContext);
         Resources resources = mContext.getResources();
@@ -1269,7 +1269,7 @@
 
         DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
         if (dpc != null) {
-            dpc.onDisplayChangedLocked();
+            dpc.onDisplayChanged();
         }
     }
 
@@ -1305,12 +1305,23 @@
         handleLogicalDisplayChangedLocked(display);
     }
 
+    private void handleLogicalDisplayDeviceStateTransitionLocked(@NonNull LogicalDisplay display) {
+        final int displayId = display.getDisplayIdLocked();
+        final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
+        if (dpc != null) {
+            dpc.onDeviceStateTransition();
+        }
+    }
+
     private Runnable updateDisplayStateLocked(DisplayDevice device) {
         // Blank or unblank the display immediately to match the state requested
         // by the display power controller (if known).
         DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
         if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) {
             final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
+            if (display == null) {
+                return null;
+            }
             final int displayId = display.getDisplayIdLocked();
             final int state = mDisplayStates.get(displayId);
 
@@ -1454,9 +1465,12 @@
         clearViewportsLocked();
 
         // Configure each display device.
-        mDisplayDeviceRepo.forEachLocked((DisplayDevice device) -> {
-            configureDisplayLocked(t, device);
-            device.performTraversalLocked(t);
+        mLogicalDisplayMapper.forEachLocked((LogicalDisplay display) -> {
+            final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+            if (device != null) {
+                configureDisplayLocked(t, device);
+                device.performTraversalLocked(t);
+            }
         });
 
         // Tell the input system about these new viewports.
@@ -2154,6 +2168,10 @@
                 case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED:
                     handleLogicalDisplayFrameRateOverridesChangedLocked(display);
                     break;
+
+                case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION:
+                    handleLogicalDisplayDeviceStateTransitionLocked(display);
+                    break;
             }
         }
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 5cd0534..6170505 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -763,13 +763,21 @@
      * when displays get swapped on foldable devices.  For example, different brightness properties
      * of each display need to be properly reflected in AutomaticBrightnessController.
      */
-    public void onDisplayChangedLocked() {
+    public void onDisplayChanged() {
         // TODO: b/175821789 - Support high brightness on multiple (folding) displays
-
         mUniqueDisplayId = mLogicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
     }
 
     /**
+     * Called when the displays are preparing to transition from one device state to another.
+     * This process involves turning off some displays so we need updatePowerState() to run and
+     * calculate the new state.
+     */
+    public void onDeviceStateTransition() {
+        sendUpdatePowerState();
+    }
+
+    /**
      * Unregisters all listeners and interrupts all running threads; halting future work.
      *
      * This method should be called when the DisplayPowerController is no longer in use; i.e. when
@@ -1004,7 +1012,9 @@
             mIgnoreProximityUntilChanged = false;
         }
 
-        if (!mLogicalDisplay.isEnabled() || mScreenOffBecauseOfProximity) {
+        if (!mLogicalDisplay.isEnabled()
+                || mLogicalDisplay.getPhase() == LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION
+                || mScreenOffBecauseOfProximity) {
             state = Display.STATE_OFF;
         }
 
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 1589419..9acb4c8 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Point;
@@ -65,6 +66,33 @@
 final class LogicalDisplay {
     private static final String TAG = "LogicalDisplay";
 
+    /**
+     * Phase indicating the logical display's existence is hidden from the rest of the framework.
+     * This can happen if the current layout has specifically requested to keep this display
+     * disabled.
+     */
+    static final int DISPLAY_PHASE_DISABLED = -1;
+
+    /**
+     * Phase indicating that the logical display is going through a layout transition.
+     * When in this phase, other systems can choose to special case power-state handling of a
+     * display that might be in a transition.
+     */
+    static final int DISPLAY_PHASE_LAYOUT_TRANSITION = 0;
+
+    /**
+     * The display is exposed to the rest of the system and its power state is determined by a
+     * power-request from PowerManager.
+     */
+    static final int DISPLAY_PHASE_ENABLED = 1;
+
+    @IntDef(prefix = {"DISPLAY_PHASE" }, value = {
+        DISPLAY_PHASE_DISABLED,
+        DISPLAY_PHASE_LAYOUT_TRANSITION,
+        DISPLAY_PHASE_ENABLED
+    })
+    @interface DisplayPhase {}
+
     // The layer stack we use when the display has been blanked to prevent any
     // of its content from appearing.
     private static final int BLANK_LAYER_STACK = -1;
@@ -129,10 +157,12 @@
     private final Rect mTempDisplayRect = new Rect();
 
     /**
-     * Indicates that the Logical display is enabled (default). See {@link #setEnabled} for
-     * more information.
+     * Indicates the current phase of the display. Generally, phases supersede any
+     * requests from PowerManager in DPC's calculation for the display state. Only when the
+     * phase is ENABLED does PowerManager's request for the display take effect.
      */
-    private boolean mIsEnabled = true;
+    @DisplayPhase
+    private int mPhase = DISPLAY_PHASE_ENABLED;
 
     /**
      * The UID mappings for refresh rate override
@@ -721,27 +751,32 @@
         return old;
     }
 
-    /**
-     * Sets the LogicalDisplay to be enabled or disabled. If the display is not enabled,
-     * the system will always set the display to power off, regardless of the global state of the
-     * device.
-     * TODO: b/170498827 - Remove when updateDisplayStateLocked is updated.
-     */
-    public void setEnabled(boolean isEnabled) {
-        mIsEnabled = isEnabled;
+    public void setPhase(@DisplayPhase int phase) {
+        mPhase = phase;
     }
 
     /**
-     * @return {@code true} iff the LogicalDisplay is enabled or {@code false}
-     * if disabled indicating that the display has been forced to be OFF.
+     * Returns the currently set phase for this LogicalDisplay. Phases are used when transitioning
+     * from one device state to another. {@see LogicalDisplayMapper}.
+     */
+    @DisplayPhase
+    public int getPhase() {
+        return mPhase;
+    }
+
+    /**
+     * @return {@code true} if the LogicalDisplay is enabled or {@code false}
+     * if disabled indicating that the display should be hidden from the rest of the apps and
+     * framework.
      */
     public boolean isEnabled() {
-        return mIsEnabled;
+        // DISPLAY_PHASE_LAYOUT_TRANSITION is still considered an 'enabled' phase.
+        return mPhase == DISPLAY_PHASE_ENABLED || mPhase == DISPLAY_PHASE_LAYOUT_TRANSITION;
     }
 
     public void dumpLocked(PrintWriter pw) {
         pw.println("mDisplayId=" + mDisplayId);
-        pw.println("mIsEnabled=" + mIsEnabled);
+        pw.println("mPhase=" + mPhase);
         pw.println("mLayerStack=" + mLayerStack);
         pw.println("mHasContent=" + mHasContent);
         pw.println("mDesiredDisplayModeSpecs={" + mDesiredDisplayModeSpecs + "}");
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index fcfa674..4c9a2d7 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -16,18 +16,24 @@
 
 package com.android.server.display;
 
+import android.annotation.NonNull;
+import android.content.Context;
 import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.DisplayInfo;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.LogicalDisplay.DisplayPhase;
 import com.android.server.display.layout.Layout;
 
 import java.io.PrintWriter;
@@ -55,11 +61,20 @@
     public static final int LOGICAL_DISPLAY_EVENT_REMOVED = 3;
     public static final int LOGICAL_DISPLAY_EVENT_SWAPPED = 4;
     public static final int LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED = 5;
+    public static final int LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION = 6;
 
     public static final int DISPLAY_GROUP_EVENT_ADDED = 1;
     public static final int DISPLAY_GROUP_EVENT_CHANGED = 2;
     public static final int DISPLAY_GROUP_EVENT_REMOVED = 3;
 
+    private static final int TIMEOUT_STATE_TRANSITION_MILLIS = 500;
+
+    private static final int MSG_TRANSITION_TO_PENDING_DEVICE_STATE = 1;
+
+    private static final int UPDATE_STATE_NEW = 0;
+    private static final int UPDATE_STATE_TRANSITION = 1;
+    private static final int UPDATE_STATE_UPDATED = 2;
+
     /**
      * Temporary display info, used for comparing display configurations.
      */
@@ -76,6 +91,11 @@
     private final boolean mSingleDisplayDemoMode;
 
     /**
+     * True if the device can have more than one internal display on at a time.
+     */
+    private final boolean mSupportsConcurrentInternalDisplays;
+
+    /**
      * Map of all logical displays indexed by logical display id.
      * Any modification to mLogicalDisplays must invalidate the DisplayManagerGlobal cache.
      * TODO: multi-display - Move the aforementioned comment?
@@ -89,13 +109,16 @@
     private final DisplayDeviceRepository mDisplayDeviceRepo;
     private final DeviceStateToLayoutMap mDeviceStateToLayoutMap;
     private final Listener mListener;
+    private final DisplayManagerService.SyncRoot mSyncRoot;
+    private final LogicalDisplayMapperHandler mHandler;
 
     /**
      * Has an entry for every logical display that the rest of the system has been notified about.
      * Any entry in here requires us to send a {@link  LOGICAL_DISPLAY_EVENT_REMOVED} event when it
-     * is deleted or {@link  LOGICAL_DISPLAY_EVENT_CHANGED} when it is changed.
+     * is deleted or {@link  LOGICAL_DISPLAY_EVENT_CHANGED} when it is changed. The values are any
+     * of the {@code UPDATE_STATE_*} constant types.
      */
-    private final SparseBooleanArray mUpdatedLogicalDisplays = new SparseBooleanArray();
+    private final SparseIntArray mUpdatedLogicalDisplays = new SparseIntArray();
 
     /**
      * Keeps track of all the display groups that we already told other people about. IOW, if a
@@ -119,11 +142,18 @@
     private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
     private Layout mCurrentLayout = null;
     private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
+    private int mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
 
-    LogicalDisplayMapper(DisplayDeviceRepository repo, Listener listener) {
+    LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo,
+            @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
+            @NonNull Handler handler) {
+        mSyncRoot = syncRoot;
+        mHandler = new LogicalDisplayMapperHandler(handler.getLooper());
         mDisplayDeviceRepo = repo;
         mListener = listener;
         mSingleDisplayDemoMode = SystemProperties.getBoolean("persist.demo.singledisplay", false);
+        mSupportsConcurrentInternalDisplays = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_supportsConcurrentInternalDisplays);
         mDisplayDeviceRepo.addListener(this);
         mDeviceStateToLayoutMap = new DeviceStateToLayoutMap();
     }
@@ -142,6 +172,7 @@
                 if (DEBUG) {
                     Slog.d(TAG, "Display device changed: " + device.getDisplayDeviceInfoLocked());
                 }
+                finishStateTransitionLocked(false /*force*/);
                 updateLogicalDisplaysLocked();
                 break;
 
@@ -166,7 +197,7 @@
     public LogicalDisplay getDisplayLocked(DisplayDevice device) {
         final int count = mLogicalDisplays.size();
         for (int i = 0; i < count; i++) {
-            LogicalDisplay display = mLogicalDisplays.valueAt(i);
+            final LogicalDisplay display = mLogicalDisplays.valueAt(i);
             if (display.getPrimaryDisplayDeviceLocked() == device) {
                 return display;
             }
@@ -198,6 +229,7 @@
         }
     }
 
+    @VisibleForTesting
     public int getDisplayGroupIdFromDisplayIdLocked(int displayId) {
         final LogicalDisplay display = getDisplayLocked(displayId);
         if (display == null) {
@@ -225,7 +257,6 @@
         ipw.increaseIndent();
 
         ipw.println("mSingleDisplayDemoMode=" + mSingleDisplayDemoMode);
-
         ipw.println("mCurrentLayout=" + mCurrentLayout);
 
         final int logicalDisplayCount = mLogicalDisplays.size();
@@ -244,19 +275,78 @@
     }
 
     void setDeviceStateLocked(int state) {
-        if (state != mDeviceState) {
-            resetLayoutLocked();
-            mDeviceState = state;
-            applyLayoutLocked();
-            updateLogicalDisplaysLocked();
+        Slog.i(TAG, "Requesting Transition to state: " + state);
+        // As part of a state transition, we may need to turn off some displays temporarily so that
+        // the transition is smooth. Plus, on some devices, only one internal displays can be
+        // on at a time. We use DISPLAY_PHASE_LAYOUT_TRANSITION to mark a display that needs to be
+        // temporarily turned off.
+        if (mDeviceState != DeviceStateManager.INVALID_DEVICE_STATE) {
+            resetLayoutLocked(mDeviceState, state, LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION);
+        }
+        mPendingDeviceState = state;
+        if (areAllTransitioningDisplaysOffLocked()) {
+            // Nothing to wait on, we're good to go
+            transitionToPendingStateLocked();
+            return;
+        }
+
+        if (DEBUG) {
+            Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState);
+        }
+        // Send the transitioning phase updates to DisplayManager so that the displays can
+        // start turning OFF in preparation for the new layout.
+        updateLogicalDisplaysLocked();
+        mHandler.sendEmptyMessageDelayed(MSG_TRANSITION_TO_PENDING_DEVICE_STATE,
+                TIMEOUT_STATE_TRANSITION_MILLIS);
+    }
+
+    private boolean areAllTransitioningDisplaysOffLocked() {
+        final int count = mLogicalDisplays.size();
+        for (int i = 0; i < count; i++) {
+            final LogicalDisplay display = mLogicalDisplays.valueAt(i);
+            if (display.getPhase() != LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION) {
+                continue;
+            }
+
+            final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+            if (device != null) {
+                final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
+                if (info.state != Display.STATE_OFF) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private void transitionToPendingStateLocked() {
+        resetLayoutLocked(mDeviceState, mPendingDeviceState, LogicalDisplay.DISPLAY_PHASE_ENABLED);
+        mDeviceState = mPendingDeviceState;
+        mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
+        applyLayoutLocked();
+        updateLogicalDisplaysLocked();
+    }
+
+    private void finishStateTransitionLocked(boolean force) {
+        if (mPendingDeviceState == DeviceStateManager.INVALID_DEVICE_STATE) {
+            return;
+        }
+
+        final boolean displaysOff = areAllTransitioningDisplaysOffLocked();
+        if (displaysOff || force) {
+            transitionToPendingStateLocked();
+            mHandler.removeMessages(MSG_TRANSITION_TO_PENDING_DEVICE_STATE);
+        } else if (DEBUG) {
+            Slog.d(TAG, "Not yet ready to transition to state=" + mPendingDeviceState
+                    + " with displays-off=" + displaysOff + " and force=" + force);
         }
     }
 
     private void handleDisplayDeviceAddedLocked(DisplayDevice device) {
         DisplayDeviceInfo deviceInfo = device.getDisplayDeviceInfoLocked();
         // Internal Displays need to have additional initialization.
-        // TODO: b/168208162 - This initializes a default dynamic display layout for INTERNAL
-        // devices, which will eventually just be a fallback in case no static layout definitions
+        // This initializes a default dynamic display layout for INTERNAL
+        // devices, which is used as a fallback in case no static layout definitions
         // exist or cannot be loaded.
         if (deviceInfo.type == Display.TYPE_INTERNAL) {
             initializeInternalDisplayDeviceLocked(device);
@@ -289,7 +379,8 @@
 
             display.updateLocked(mDisplayDeviceRepo);
             final DisplayInfo newDisplayInfo = display.getDisplayInfoLocked();
-            final boolean wasPreviouslyUpdated = mUpdatedLogicalDisplays.get(displayId);
+            final int updateState = mUpdatedLogicalDisplays.get(displayId, UPDATE_STATE_NEW);
+            final boolean wasPreviouslyUpdated = updateState != UPDATE_STATE_NEW;
 
             // The display is no longer valid and needs to be removed.
             if (!display.isValidLocked()) {
@@ -331,6 +422,10 @@
                 assignDisplayGroupLocked(display);
                 mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_CHANGED);
 
+            } else if (updateState == UPDATE_STATE_TRANSITION) {
+                mLogicalDisplaysToUpdate.put(displayId,
+                        LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION);
+
             // Display frame rate overrides changed.
             } else if (!display.getPendingFrameRateOverrideUids().isEmpty()) {
                 mLogicalDisplaysToUpdate.put(
@@ -347,7 +442,7 @@
                 }
             }
 
-            mUpdatedLogicalDisplays.put(displayId, true);
+            mUpdatedLogicalDisplays.put(displayId, UPDATE_STATE_UPDATED);
         }
 
         // Go through the groups and do the same thing. We do this after displays since group
@@ -376,12 +471,13 @@
 
         // Send the display and display group updates in order by message type. This is important
         // to ensure that addition and removal notifications happen in the right order.
+        sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION);
         sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_ADDED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REMOVED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED);
-        sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_ADDED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED);
+        sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_ADDED);
         sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_CHANGED);
         sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_REMOVED);
 
@@ -400,7 +496,14 @@
             }
 
             final int id = mLogicalDisplaysToUpdate.keyAt(i);
-            mListener.onLogicalDisplayEventLocked(getDisplayLocked(id), msg);
+            final LogicalDisplay display = getDisplayLocked(id);
+            if (DEBUG) {
+                final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+                final String uniqueId = device == null ? "null" : device.getUniqueId();
+                Slog.d(TAG, "Sending " + displayEventToString(msg) + " for display=" + id
+                        + " with device=" + uniqueId);
+            }
+            mListener.onLogicalDisplayEventLocked(display, msg);
             if (msg == LOGICAL_DISPLAY_EVENT_REMOVED) {
                 // We wait until we sent the EVENT_REMOVED event before actually removing the
                 // display.
@@ -464,36 +567,81 @@
     }
 
     /**
-     * Resets the current layout in preparation for a new layout. Layouts can specify if some
-     * displays should be disabled (OFF). When switching from one layout to another, we go
-     * through each of the displays and make sure any displays we might have disabled are
-     * enabled again.
+     * Goes through all the displays used in the layouts for the specified {@code fromState} and
+     * {@code toState} and applies the specified {@code phase}. When a new layout is requested, we
+     * put the displays that will change into a transitional phase so that they can all be turned
+     * OFF. Once all are confirmed OFF, then this method gets called again to reset the phase to
+     * normal operation. This helps to ensure that all display-OFF requests are made before
+     * display-ON which in turn hides any resizing-jank windows might incur when switching displays.
+     *
+     * @param fromState The state we are switching from.
+     * @param toState The state we are switching to.
+     * @param phase The new phase to apply to the displays.
      */
-    private void resetLayoutLocked() {
-        final Layout layout = mDeviceStateToLayoutMap.get(mDeviceState);
-        for (int i = layout.size() - 1; i >= 0; i--) {
-            final Layout.Display displayLayout = layout.getAt(i);
-            final LogicalDisplay display = getDisplayLocked(displayLayout.getLogicalDisplayId());
-            if (display != null) {
-                enableDisplayLocked(display, true); // Reset all displays back to enabled
+    private void resetLayoutLocked(int fromState, int toState, @DisplayPhase int phase) {
+        final Layout fromLayout = mDeviceStateToLayoutMap.get(fromState);
+        final Layout toLayout = mDeviceStateToLayoutMap.get(toState);
+
+        final int count = mLogicalDisplays.size();
+        for (int i = 0; i < count; i++) {
+            final LogicalDisplay logicalDisplay = mLogicalDisplays.valueAt(i);
+            final int displayId = logicalDisplay.getDisplayIdLocked();
+            final DisplayDevice device = logicalDisplay.getPrimaryDisplayDeviceLocked();
+            if (device == null) {
+                // If there's no device, then the logical display is due to be removed. Ignore it.
+                continue;
+            }
+
+            // Grab the display associations this display-device has in the old layout and the
+            // new layout.
+            final DisplayAddress address = device.getDisplayDeviceInfoLocked().address;
+
+            // Virtual displays do not have addresses.
+            final Layout.Display fromDisplay =
+                    address != null ? fromLayout.getByAddress(address) : null;
+            final Layout.Display toDisplay =
+                    address != null ? toLayout.getByAddress(address) : null;
+
+            // If a layout doesn't mention a display-device at all, then the display-device defaults
+            // to enabled. This is why we treat null as "enabled" in the code below.
+            final boolean wasEnabled = fromDisplay == null || fromDisplay.isEnabled();
+            final boolean willBeEnabled = toDisplay == null || toDisplay.isEnabled();
+
+            final boolean deviceHasNewLogicalDisplayId = fromDisplay != null && toDisplay != null
+                    && fromDisplay.getLogicalDisplayId() != toDisplay.getLogicalDisplayId();
+
+            // We consider a display-device as changing/transition if
+            // 1) It's already marked as transitioning
+            // 2) It's going from enabled to disabled
+            // 3) It's enabled, but it's mapped to a new logical display ID. To the user this
+            //    would look like apps moving from one screen to another since task-stacks stay
+            //    with the logical display [ID].
+            final boolean isTransitioning =
+                    (logicalDisplay.getPhase() == LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION)
+                    || (wasEnabled && !willBeEnabled)
+                    || (wasEnabled && deviceHasNewLogicalDisplayId);
+
+            if (isTransitioning) {
+                setDisplayPhase(logicalDisplay, phase);
+                if (phase == LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION) {
+                    mUpdatedLogicalDisplays.put(displayId, UPDATE_STATE_TRANSITION);
+                }
             }
         }
     }
 
-
     /**
      * Apply (or reapply) the currently selected display layout.
      */
     private void applyLayoutLocked() {
-        final Layout layout = mDeviceStateToLayoutMap.get(mDeviceState);
-        mCurrentLayout = layout;
-        Slog.i(TAG, "Applying the display layout for device state(" + mDeviceState
-                + "): " + layout);
+        final Layout oldLayout = mCurrentLayout;
+        mCurrentLayout = mDeviceStateToLayoutMap.get(mDeviceState);
+        Slog.i(TAG, "Applying layout: " + mCurrentLayout + ", Previous layout: " + oldLayout);
 
         // Go through each of the displays in the current layout set.
-        final int size = layout.size();
+        final int size = mCurrentLayout.size();
         for (int i = 0; i < size; i++) {
-            final Layout.Display displayLayout = layout.getAt(i);
+            final Layout.Display displayLayout = mCurrentLayout.getAt(i);
 
             // If the underlying display-device we want to use for this display
             // doesn't exist, then skip it. This can happen at startup as display-devices
@@ -521,8 +669,12 @@
             if (newDisplay != oldDisplay) {
                 newDisplay.swapDisplaysLocked(oldDisplay);
             }
-            enableDisplayLocked(newDisplay, displayLayout.isEnabled());
+
+            if (!displayLayout.isEnabled()) {
+                setDisplayPhase(newDisplay, LogicalDisplay.DISPLAY_PHASE_DISABLED);
+            }
         }
+
     }
 
 
@@ -540,23 +692,23 @@
         final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device);
         display.updateLocked(mDisplayDeviceRepo);
         mLogicalDisplays.put(displayId, display);
-        enableDisplayLocked(display, device != null);
+        setDisplayPhase(display, LogicalDisplay.DISPLAY_PHASE_ENABLED);
         return display;
     }
 
-    private void enableDisplayLocked(LogicalDisplay display, boolean isEnabled) {
+    private void setDisplayPhase(LogicalDisplay display, @DisplayPhase int phase) {
         final int displayId = display.getDisplayIdLocked();
         final DisplayInfo info = display.getDisplayInfoLocked();
 
         final boolean disallowSecondaryDisplay = mSingleDisplayDemoMode
                 && (info.type != Display.TYPE_INTERNAL);
-        if (isEnabled && disallowSecondaryDisplay) {
+        if (phase != LogicalDisplay.DISPLAY_PHASE_DISABLED && disallowSecondaryDisplay) {
             Slog.i(TAG, "Not creating a logical display for a secondary display because single"
                     + " display demo mode is enabled: " + display.getDisplayInfoLocked());
-            isEnabled = false;
+            phase = LogicalDisplay.DISPLAY_PHASE_DISABLED;
         }
 
-        display.setEnabled(isEnabled);
+        display.setPhase(phase);
     }
 
     private int assignDisplayGroupIdLocked(boolean isOwnDisplayGroup) {
@@ -564,14 +716,15 @@
     }
 
     private void initializeInternalDisplayDeviceLocked(DisplayDevice device) {
-        // We always want to make sure that our default display layout creates a logical
+        // We always want to make sure that our default layout creates a logical
         // display for every internal display device that is found.
         // To that end, when we are notified of a new internal display, we add it to
-        // the default definition if it is not already there.
-        final Layout layoutSet = mDeviceStateToLayoutMap.get(DeviceStateToLayoutMap.STATE_DEFAULT);
+        // the default layout definition if it is not already there.
+        final Layout layout = mDeviceStateToLayoutMap.get(DeviceStateToLayoutMap.STATE_DEFAULT);
         final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
         final boolean isDefault = (info.flags & DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY) != 0;
-        layoutSet.createDisplayLocked(info.address, isDefault, true /* isEnabled */);
+        final boolean isEnabled = isDefault || mSupportsConcurrentInternalDisplays;
+        layout.createDisplayLocked(info.address, isDefault, isEnabled);
     }
 
     private int assignLayerStackLocked(int displayId) {
@@ -580,9 +733,45 @@
         return displayId;
     }
 
+    private String displayEventToString(int msg) {
+        switch(msg) {
+            case LOGICAL_DISPLAY_EVENT_ADDED:
+                return "added";
+            case LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION:
+                return "transition";
+            case LOGICAL_DISPLAY_EVENT_CHANGED:
+                return "changed";
+            case LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED:
+                return "framerate_override";
+            case LOGICAL_DISPLAY_EVENT_SWAPPED:
+                return "swapped";
+            case LOGICAL_DISPLAY_EVENT_REMOVED:
+                return "removed";
+        }
+        return null;
+    }
+
     public interface Listener {
         void onLogicalDisplayEventLocked(LogicalDisplay display, int event);
         void onDisplayGroupEventLocked(int groupId, int event);
         void onTraversalRequested();
     }
+
+    private class LogicalDisplayMapperHandler extends Handler {
+        LogicalDisplayMapperHandler(Looper looper) {
+            super(looper, null, true /*async*/);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_TRANSITION_TO_PENDING_DEVICE_STATE:
+                    synchronized (mSyncRoot) {
+                        finishStateTransitionLocked(true /*force*/);
+                    }
+                    break;
+            }
+        }
+    }
+
 }
diff --git a/services/core/java/com/android/server/display/layout/Layout.java b/services/core/java/com/android/server/display/layout/Layout.java
index ef33667..e53aec1 100644
--- a/services/core/java/com/android/server/display/layout/Layout.java
+++ b/services/core/java/com/android/server/display/layout/Layout.java
@@ -19,6 +19,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.util.Slog;
 import android.view.DisplayAddress;
 
@@ -100,11 +101,28 @@
      *
      * @return The display corresponding to the specified display ID.
      */
+    @Nullable
     public Display getById(int id) {
         for (int i = 0; i < mDisplays.size(); i++) {
-            Display layout = mDisplays.get(i);
-            if (id == layout.getLogicalDisplayId()) {
-                return layout;
+            Display display = mDisplays.get(i);
+            if (id == display.getLogicalDisplayId()) {
+                return display;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param address The display address to check.
+     *
+     * @return The display corresponding to the specified address.
+     */
+    @Nullable
+    public Display getByAddress(@NonNull DisplayAddress address) {
+        for (int i = 0; i < mDisplays.size(); i++) {
+            Display display = mDisplays.get(i);
+            if (address.equals(display.getAddress())) {
+                return display;
             }
         }
         return null;
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index d784a22..8279624 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -27,17 +27,20 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.PropertyInvalidatedCache;
 import android.content.Context;
+import android.content.res.Resources;
+import android.os.Handler;
 import android.os.Parcel;
 import android.os.Process;
+import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.DisplayInfo;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -61,9 +64,12 @@
 
     private DisplayDeviceRepository mDisplayDeviceRepo;
     private LogicalDisplayMapper mLogicalDisplayMapper;
-    private Context mContext;
+    private TestLooper mLooper;
+    private Handler mHandler;
 
     @Mock LogicalDisplayMapper.Listener mListenerMock;
+    @Mock Context mContextMock;
+    @Mock Resources mResourcesMock;
 
     @Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor;
 
@@ -73,7 +79,6 @@
         System.setProperty("dexmaker.share_classloader", "true");
         MockitoAnnotations.initMocks(this);
 
-        mContext = InstrumentationRegistry.getContext();
         mDisplayDeviceRepo = new DisplayDeviceRepository(
                 new DisplayManagerService.SyncRoot(),
                 new PersistentDataStore(new PersistentDataStore.Injector() {
@@ -94,7 +99,15 @@
         // Disable binder caches in this process.
         PropertyInvalidatedCache.disableForTestMode();
 
-        mLogicalDisplayMapper = new LogicalDisplayMapper(mDisplayDeviceRepo, mListenerMock);
+        when(mContextMock.getResources()).thenReturn(mResourcesMock);
+        when(mResourcesMock.getBoolean(
+                com.android.internal.R.bool.config_supportsConcurrentInternalDisplays))
+                .thenReturn(true);
+
+        mLooper = new TestLooper();
+        mHandler = new Handler(mLooper.getLooper());
+        mLogicalDisplayMapper = new LogicalDisplayMapper(mContextMock, mDisplayDeviceRepo,
+                mListenerMock, new DisplayManagerService.SyncRoot(), mHandler);
     }
 
 
@@ -299,7 +312,7 @@
         private DisplayDeviceInfo mSentInfo;
 
         TestDisplayDevice() {
-            super(null, null, "test_display_" + sUniqueTestDisplayId++, mContext);
+            super(null, null, "test_display_" + sUniqueTestDisplayId++, mContextMock);
             mInfo = new DisplayDeviceInfo();
         }