Add APIs to control the external display state

External displays start disabled, signal connection / disconnection and
can be enabled / disabled by system apps.

The implementation of the signalling is mostly in LogicalDisplayMapper.

The flags are accessed through DisplayManagerFlags, which is injected
into DisplayManagerService. All other uses come from
DisplayManagerService and is propagated. This allows for injection in
tests.

Currently, we are using a standard flag, but if DeviceConfig is not
ready, we instead read a default static value. The value cannot change
during the runtime of the device.

When read only flags are ready, we can simplify the code to remove the
default value, as the flag will always be available.

To test on a local device, set
`DisplayManagerFlags.DEFAULT_IS_CONNECTED_DISPLAY_MANAGEMENT_ENABLED` to true.

Bug: 280739508
Test: atest LocalDisplayAdapterTest
Test: atest DisplayManagerServiceTest
Change-Id: I91411a8e726982d88474cf2ecd0f423bde9be107
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 4323bf8..f0c87a1 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -554,7 +554,8 @@
             EVENT_FLAG_DISPLAY_CHANGED,
             EVENT_FLAG_DISPLAY_REMOVED,
             EVENT_FLAG_DISPLAY_BRIGHTNESS,
-            EVENT_FLAG_HDR_SDR_RATIO_CHANGED
+            EVENT_FLAG_HDR_SDR_RATIO_CHANGED,
+            EVENT_FLAG_DISPLAY_CONNECTION_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EventsMask {}
@@ -610,6 +611,13 @@
      */
     public static final long EVENT_FLAG_HDR_SDR_RATIO_CHANGED = 1L << 4;
 
+    /**
+     * Event flag to register for a display's connection changed.
+     *
+     * @hide
+     */
+    public static final long EVENT_FLAG_DISPLAY_CONNECTION_CHANGED = 1L << 5;
+
     /** @hide */
     public DisplayManager(Context context) {
         mContext = context;
@@ -891,6 +899,25 @@
     }
 
     /**
+     * Enable a connected display that is currently disabled.
+     * @hide
+     */
+    @RequiresPermission("android.permission.MANAGE_DISPLAYS")
+    public void enableConnectedDisplay(int displayId) {
+        mGlobal.enableConnectedDisplay(displayId);
+    }
+
+
+    /**
+     * Disable a connected display that is currently enabled.
+     * @hide
+     */
+    @RequiresPermission("android.permission.MANAGE_DISPLAYS")
+    public void disableConnectedDisplay(int displayId) {
+        mGlobal.disableConnectedDisplay(displayId);
+    }
+
+    /**
      * Set the level of color saturation to apply to the display.
      * @param level The amount of saturation to apply, between 0 and 1 inclusive.
      * 0 produces a grayscale image, 1 is normal.
@@ -1633,6 +1660,24 @@
          * @param displayId The id of the logical display that changed.
          */
         void onDisplayChanged(int displayId);
+
+        /**
+         * Called when a display is connected, but not necessarily used.
+         *
+         * A display is always connected before being added.
+         * @hide
+         */
+        default void onDisplayConnected(int displayId) { }
+
+        /**
+         * Called when a display is disconnected.
+         *
+         * If a display was added, a display is only disconnected after it has been removed. Note,
+         * however, that the display may have been disconnected by the time the removed event is
+         * received by the listener.
+         * @hide
+         */
+        default void onDisplayDisconnected(int displayId) { }
     }
 
     /**
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 3be82bc..6d6085b 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -90,6 +90,8 @@
             EVENT_DISPLAY_REMOVED,
             EVENT_DISPLAY_BRIGHTNESS_CHANGED,
             EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED,
+            EVENT_DISPLAY_CONNECTED,
+            EVENT_DISPLAY_DISCONNECTED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface DisplayEvent {}
@@ -99,6 +101,8 @@
     public static final int EVENT_DISPLAY_REMOVED = 3;
     public static final int EVENT_DISPLAY_BRIGHTNESS_CHANGED = 4;
     public static final int EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED = 5;
+    public static final int EVENT_DISPLAY_CONNECTED = 6;
+    public static final int EVENT_DISPLAY_DISCONNECTED = 7;
 
     @UnsupportedAppUsage
     private static DisplayManagerGlobal sInstance;
@@ -414,6 +418,9 @@
 
     private void updateCallbackIfNeededLocked() {
         int mask = calculateEventsMaskLocked();
+        if (DEBUG) {
+            Log.d(TAG, "Mask for listener: " + mask);
+        }
         if (mask != mRegisteredEventsMask) {
             try {
                 mDm.registerCallbackWithEventMask(mCallback, mask);
@@ -459,6 +466,33 @@
         }
     }
 
+    /**
+     * Enable a connected display that is currently disabled.
+     * @hide
+     */
+    @RequiresPermission("android.permission.MANAGE_DISPLAYS")
+    public void enableConnectedDisplay(int displayId) {
+        try {
+            mDm.enableConnectedDisplay(displayId);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Error trying to enable external display", ex);
+        }
+    }
+
+
+    /**
+     * Disable a connected display that is currently enabled.
+     * @hide
+     */
+    @RequiresPermission("android.permission.MANAGE_DISPLAYS")
+    public void disableConnectedDisplay(int displayId) {
+        try {
+            mDm.disableConnectedDisplay(displayId);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Error trying to enable external display", ex);
+        }
+    }
+
     public void startWifiDisplayScan() {
         synchronized (mLock) {
             if (mWifiDisplayScanNestCount++ == 0) {
@@ -1179,6 +1213,16 @@
                         mListener.onDisplayChanged(msg.arg1);
                     }
                     break;
+                case EVENT_DISPLAY_CONNECTED:
+                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
+                        mListener.onDisplayConnected(msg.arg1);
+                    }
+                    break;
+                case EVENT_DISPLAY_DISCONNECTED:
+                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
+                        mListener.onDisplayDisconnected(msg.arg1);
+                    }
+                    break;
             }
             if (DEBUG) {
                 Trace.endSection();
@@ -1302,6 +1346,10 @@
                 return "BRIGHTNESS_CHANGED";
             case EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED:
                 return "HDR_SDR_RATIO_CHANGED";
+            case EVENT_DISPLAY_CONNECTED:
+                return "EVENT_DISPLAY_CONNECTED";
+            case EVENT_DISPLAY_DISCONNECTED:
+                return "EVENT_DISPLAY_DISCONNECTED";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 18edbdb..83de4e4 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -227,4 +227,12 @@
 
     // Query overlay properties of the device
     OverlayProperties getOverlaySupport();
+
+    // Enable a connected display that is disabled.
+    @EnforcePermission("MANAGE_DISPLAYS")
+    void enableConnectedDisplay(int displayId);
+
+    // Disable a connected display that is enabled.
+    @EnforcePermission("MANAGE_DISPLAYS")
+    void disableConnectedDisplay(int displayId);
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c5b9ee9..f3194b4 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7706,6 +7706,15 @@
     <permission android:name="android.permission.WRITE_FLAGS"
         android:protectionLevel="signature" />
 
+    <!-- @hide Allows internal applications to manage displays.
+        <p>This means intercept internal signals about displays being (dis-)connected
+        and being able to enable or disable the external displays.
+        <p>Not for use by third-party applications.
+        <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.MANAGE_DISPLAYS"
+        android:protectionLevel="signature" />
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 323f65f..bd3b83e 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -846,6 +846,8 @@
     <uses-permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR" />
     <!-- Permission required for CTS test IntentRedirectionTest -->
     <uses-permission android:name="android.permission.QUERY_CLONED_APPS" />
+    <!-- Permission required for adb display commands `enable-display` and `disable-display`. -->
+    <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
     <!-- Permission required for accessing all content provider mime types -->
     <uses-permission android:name="android.permission.GET_ANY_PROVIDER_TYPE" />
 
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 66a77a3..714ff68 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -181,6 +181,7 @@
         "android.hardware.power.stats-V2-java",
         "android.hidl.manager-V1.2-java",
         "cbor-java",
+        "display_flags_lib",
         "icu4j_calendar_astronomer",
         "netd-client",
         "overlayable_policy_aidl-java",
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index fe445a6..b994105 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -21,6 +21,7 @@
 import static android.Manifest.permission.CAPTURE_SECURE_VIDEO_OUTPUT;
 import static android.Manifest.permission.CAPTURE_VIDEO_OUTPUT;
 import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
+import static android.Manifest.permission.MANAGE_DISPLAYS;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
 import static android.hardware.display.DisplayManager.EventsMask;
@@ -44,6 +45,7 @@
 import static android.os.Process.ROOT_UID;
 
 import android.Manifest;
+import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -152,6 +154,7 @@
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.display.DisplayDeviceConfig.SensorData;
 import com.android.server.display.feature.DeviceConfigParameterProvider;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.Layout;
 import com.android.server.display.mode.DisplayModeDirector;
 import com.android.server.display.utils.SensorUtils;
@@ -509,6 +512,8 @@
 
     private final DeviceConfigParameterProvider mConfigParameterProvider;
 
+    private final DisplayManagerFlags mFlags;
+
     /**
      * Applications use {@link android.view.Display#getRefreshRate} and
      * {@link android.view.Display.Mode#getRefreshRate} to know what is the display refresh rate.
@@ -538,12 +543,13 @@
         super(context);
         mInjector = injector;
         mContext = context;
+        mFlags = injector.getFlags();
         mHandler = new DisplayManagerHandler(DisplayThread.get().getLooper());
         mUiHandler = UiThread.getHandler();
         mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore);
         mLogicalDisplayMapper = new LogicalDisplayMapper(mContext, mDisplayDeviceRepo,
                 new LogicalDisplayListener(), mSyncRoot, mHandler,
-                new FoldSettingWrapper(mContext.getContentResolver()));
+                new FoldSettingWrapper(mContext.getContentResolver()), mFlags);
         mDisplayModeDirector = new DisplayModeDirector(context, mHandler);
         mBrightnessSynchronizer = new BrightnessSynchronizer(mContext);
         Resources resources = mContext.getResources();
@@ -745,6 +751,11 @@
     }
 
     @VisibleForTesting
+    LogicalDisplayMapper getLogicalDisplayMapper() {
+        return mLogicalDisplayMapper;
+    }
+
+    @VisibleForTesting
     boolean isMinimalPostProcessingAllowed() {
         synchronized (mSyncRoot) {
             return mMinimalPostProcessingAllowed;
@@ -888,8 +899,14 @@
             mDisplayStates.setValueAt(index, state);
             brightnessPair.brightness = brightnessState;
             brightnessPair.sdrBrightness = sdrBrightnessState;
-            runnable = updateDisplayStateLocked(mLogicalDisplayMapper.getDisplayLocked(displayId)
-                    .getPrimaryDisplayDeviceLocked());
+            // TODO(b/297503094) Preventing disabled display from being turned on should happen
+            // elsewhere.
+            LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId);
+            if (!display.isEnabledLocked() && state != Display.STATE_OFF) {
+                // If the display is disabled, any request other than turning it off should fail.
+                return;
+            }
+            runnable = updateDisplayStateLocked(display.getPrimaryDisplayDeviceLocked());
             if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) {
                 Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
                         "requestDisplayStateInternal:" + displayId, displayId);
@@ -1796,7 +1813,17 @@
         adapter.registerLocked();
     }
 
-    private void handleLogicalDisplayAddedLocked(LogicalDisplay display) {
+    @GuardedBy("mSyncRoot")
+    private void handleLogicalDisplayDisconnectedLocked(LogicalDisplay display) {
+        if (!mFlags.isConnectedDisplayManagementEnabled()) {
+            Slog.e(TAG, "DisplayDisconnected shouldn't be received when the flag is off");
+            return;
+        }
+        releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED);
+    }
+
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
+    private void setupLogicalDisplay(LogicalDisplay display) {
         final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
         final int displayId = display.getDisplayIdLocked();
         final boolean isDefault = displayId == Display.DEFAULT_DISPLAY;
@@ -1814,8 +1841,8 @@
         } else {
             configurePreferredDisplayModeLocked(display);
         }
-        DisplayPowerControllerInterface dpc = addDisplayPowerControllerLocked(display);
 
+        DisplayPowerControllerInterface dpc = addDisplayPowerControllerLocked(display);
         if (dpc != null) {
             final int leadDisplayId = display.getLeadDisplayIdLocked();
             updateDisplayPowerControllerLeaderLocked(dpc, leadDisplayId);
@@ -1840,19 +1867,51 @@
                 new BrightnessPair(brightnessDefault, brightnessDefault));
 
         DisplayManagerGlobal.invalidateLocalDisplayInfoCaches();
+    }
+
+    private void updateLogicalDisplayState(LogicalDisplay display) {
+        Runnable work = updateDisplayStateLocked(display.getPrimaryDisplayDeviceLocked());
+        if (work != null) {
+            work.run();
+        }
+        scheduleTraversalLocked(false);
+    }
+
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private void handleLogicalDisplayConnectedLocked(LogicalDisplay display) {
+        if (!mFlags.isConnectedDisplayManagementEnabled()) {
+            Slog.e(TAG, "DisplayConnected shouldn't be received when the flag is off");
+            return;
+        }
+
+        setupLogicalDisplay(display);
+
+        // TODO(b/292196201) Remove when the display can be disabled before DPC is created.
+        if (display.getDisplayInfoLocked().type == Display.TYPE_EXTERNAL) {
+            display.setEnabledLocked(false);
+        }
+
+        sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED);
+
+        updateLogicalDisplayState(display);
+    }
+
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private void handleLogicalDisplayAddedLocked(LogicalDisplay display) {
+        final int displayId = display.getDisplayIdLocked();
+        final boolean isDefault = displayId == Display.DEFAULT_DISPLAY;
+        if (!mFlags.isConnectedDisplayManagementEnabled()) {
+            setupLogicalDisplay(display);
+        }
 
         // Wake up waitForDefaultDisplay.
         if (isDefault) {
             mSyncRoot.notifyAll();
         }
 
-        sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_ADDED);
+        sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_ADDED);
 
-        Runnable work = updateDisplayStateLocked(device);
-        if (work != null) {
-            work.run();
-        }
-        scheduleTraversalLocked(false);
+        updateLogicalDisplayState(display);
     }
 
     private void handleLogicalDisplayChangedLocked(@NonNull LogicalDisplay display) {
@@ -1865,7 +1924,13 @@
         // We don't bother invalidating the display info caches here because any changes to the
         // display info will trigger a cache invalidation inside of LogicalDisplay before we hit
         // this point.
-        sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED);
+        sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED);
+
+        applyDisplayChangedLocked(display);
+    }
+
+    private void applyDisplayChangedLocked(@NonNull LogicalDisplay display) {
+        final int displayId = display.getDisplayIdLocked();
         scheduleTraversalLocked(false);
         mPersistentDataStore.saveIfNeeded();
 
@@ -1920,7 +1985,28 @@
     }
 
     private void handleLogicalDisplayRemovedLocked(@NonNull LogicalDisplay display) {
+        // With display management, the display is removed when disabled, and it might still exist.
+        // Resources must only be released when the disconnected signal is received.
+        if (mFlags.isConnectedDisplayManagementEnabled()) {
+            if (display.isValidLocked()) {
+                updateViewportPowerStateLocked(display);
+            }
+
+            // Note: This method is only called if the display was enabled before being removed.
+            sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED);
+
+            if (display.isValidLocked()) {
+                applyDisplayChangedLocked(display);
+            }
+            return;
+        }
+
+        releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED);
+    }
+
+    private void releaseDisplayAndEmitEvent(LogicalDisplay display, int event) {
         final int displayId = display.getDisplayIdLocked();
+
         final DisplayPowerControllerInterface dpc =
                 mDisplayPowerControllers.removeReturnOld(displayId);
         if (dpc != null) {
@@ -1930,12 +2016,13 @@
         mDisplayStates.delete(displayId);
         mDisplayBrightnesses.delete(displayId);
         DisplayManagerGlobal.invalidateLocalDisplayInfoCaches();
-        sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED);
+
+        sendDisplayEventLocked(display, event);
         scheduleTraversalLocked(false);
 
         if (mDisplayWindowPolicyControllers.contains(displayId)) {
-            final IVirtualDevice virtualDevice = mDisplayWindowPolicyControllers.removeReturnOld(
-                    displayId).first;
+            final IVirtualDevice virtualDevice =
+                    mDisplayWindowPolicyControllers.removeReturnOld(displayId).first;
             if (virtualDevice != null) {
                 mHandler.post(() -> {
                     getLocalService(VirtualDeviceManagerInternal.class)
@@ -1956,7 +2043,8 @@
     }
 
     private void handleLogicalDisplayHdrSdrRatioChangedLocked(@NonNull LogicalDisplay display) {
-        sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED);
+        sendDisplayEventIfEnabledLocked(display,
+                DisplayManagerGlobal.EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED);
     }
 
     private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) {
@@ -2812,15 +2900,21 @@
         }
     }
 
-    private void sendDisplayEventLocked(@NonNull LogicalDisplay display, @DisplayEvent int event) {
+    // Send a display event if the display is enabled
+    private void sendDisplayEventIfEnabledLocked(@NonNull LogicalDisplay display,
+                                                 @DisplayEvent int event) {
         // Only send updates outside of DisplayManagerService for enabled displays
         if (display.isEnabledLocked()) {
-            int displayId = display.getDisplayIdLocked();
-            Message msg = mHandler.obtainMessage(MSG_DELIVER_DISPLAY_EVENT, displayId, event);
-            mHandler.sendMessage(msg);
+            sendDisplayEventLocked(display, event);
         }
     }
 
+    private void sendDisplayEventLocked(@NonNull LogicalDisplay display, @DisplayEvent int event) {
+        int displayId = display.getDisplayIdLocked();
+        Message msg = mHandler.obtainMessage(MSG_DELIVER_DISPLAY_EVENT, displayId, event);
+        mHandler.sendMessage(msg);
+    }
+
     private void sendDisplayGroupEvent(int groupId, int event) {
         Message msg = mHandler.obtainMessage(MSG_DELIVER_DISPLAY_GROUP_EVENT, groupId, event);
         mHandler.sendMessage(msg);
@@ -3051,6 +3145,12 @@
                 && mode.getRefreshRate() > 0.0f;
     }
 
+    void enableConnectedDisplay(int displayId, boolean enabled) {
+        synchronized (mSyncRoot) {
+            mLogicalDisplayMapper.setDisplayEnabledLocked(displayId, enabled);
+        }
+    }
+
     /**
      * This is the object that everything in the display manager locks on.
      * We make it an inner class within the {@link DisplayManagerService} to so that it is
@@ -3068,7 +3168,8 @@
         }
 
         LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
-                Handler handler, DisplayAdapter.Listener displayAdapterListener) {
+                                                   Handler handler,
+                                                   DisplayAdapter.Listener displayAdapterListener) {
             return new LocalDisplayAdapter(syncRoot, context, handler, displayAdapterListener);
         }
 
@@ -3098,6 +3199,10 @@
             IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE);
             return  IMediaProjectionManager.Stub.asInterface(b);
         }
+
+        DisplayManagerFlags getFlags() {
+            return new DisplayManagerFlags();
+        }
     }
 
     @VisibleForTesting
@@ -3167,7 +3272,8 @@
 
     private void handleBrightnessChange(LogicalDisplay display) {
         synchronized (mSyncRoot) {
-            sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_BRIGHTNESS_CHANGED);
+            sendDisplayEventIfEnabledLocked(display,
+                    DisplayManagerGlobal.EVENT_DISPLAY_BRIGHTNESS_CHANGED);
         }
     }
 
@@ -3261,6 +3367,8 @@
     }
 
     private final class LogicalDisplayListener implements LogicalDisplayMapper.Listener {
+
+        @GuardedBy("mSyncRoot")
         @Override
         public void onLogicalDisplayEventLocked(LogicalDisplay display, int event) {
             switch (event) {
@@ -3291,6 +3399,14 @@
                 case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED:
                     handleLogicalDisplayHdrSdrRatioChangedLocked(display);
                     break;
+
+                case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CONNECTED:
+                    handleLogicalDisplayConnectedLocked(display);
+                    break;
+
+                case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED:
+                    handleLogicalDisplayDisconnectedLocked(display);
+                    break;
             }
         }
 
@@ -3367,6 +3483,10 @@
                     return (mask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0;
                 case DisplayManagerGlobal.EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED:
                     return (mask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0;
+                case DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED:
+                    // fallthrough
+                case DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED:
+                    return (mask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0;
                 default:
                     // This should never happen.
                     Slog.e(TAG, "Unknown display event " + event);
@@ -3484,6 +3604,7 @@
         }
 
         @Override // Binder call
+        @SuppressLint("AndroidFrameworkRequiresPermission") // Permission only required sometimes
         public void registerCallbackWithEventMask(IDisplayManagerCallback callback,
                 @EventsMask long eventsMask) {
             if (callback == null) {
@@ -3492,6 +3613,14 @@
 
             final int callingPid = Binder.getCallingPid();
             final int callingUid = Binder.getCallingUid();
+
+            if (mFlags.isConnectedDisplayManagementEnabled()) {
+                if ((eventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
+                    mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS,
+                            "Permission required to get signals about connection events.");
+                }
+            }
+
             final long token = Binder.clearCallingIdentity();
             try {
                 registerCallbackInternal(callback, callingPid, callingUid, eventsMask);
@@ -3500,7 +3629,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
         @Override // Binder call
         public void startWifiDisplayScan() {
             startWifiDisplayScan_enforcePermission();
@@ -3514,7 +3643,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
         @Override // Binder call
         public void stopWifiDisplayScan() {
             stopWifiDisplayScan_enforcePermission();
@@ -3591,7 +3720,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
         @Override // Binder call
         public void pauseWifiDisplay() {
             pauseWifiDisplay_enforcePermission();
@@ -3604,7 +3733,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
         @Override // Binder call
         public void resumeWifiDisplay() {
             resumeWifiDisplay_enforcePermission();
@@ -3630,7 +3759,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+        @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
         @Override // Binder call
         public void setUserDisabledHdrTypes(int[] userDisabledFormats) {
             setUserDisabledHdrTypes_enforcePermission();
@@ -3656,7 +3785,7 @@
             DisplayControl.overrideHdrTypes(displayToken, modes);
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+        @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
         @Override // Binder call
         public void setAreUserDisabledHdrTypesAllowed(boolean areUserDisabledHdrTypesAllowed) {
             setAreUserDisabledHdrTypesAllowed_enforcePermission();
@@ -3682,7 +3811,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_COLOR_MODE)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_COLOR_MODE)
         @Override // Binder call
         public void requestColorMode(int displayId, int colorMode) {
             requestColorMode_enforcePermission();
@@ -3762,7 +3891,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.BRIGHTNESS_SLIDER_USAGE)
+        @EnforcePermission(android.Manifest.permission.BRIGHTNESS_SLIDER_USAGE)
         @Override // Binder call
         public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents(String callingPackage) {
             getBrightnessEvents_enforcePermission();
@@ -3794,7 +3923,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_AMBIENT_LIGHT_STATS)
+        @EnforcePermission(android.Manifest.permission.ACCESS_AMBIENT_LIGHT_STATS)
         @Override // Binder call
         public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats() {
             getAmbientBrightnessStats_enforcePermission();
@@ -3811,7 +3940,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
         @Override // Binder call
         public void setBrightnessConfigurationForUser(
                 BrightnessConfiguration c, @UserIdInt int userId, String packageName) {
@@ -3840,7 +3969,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
         @Override // Binder call
         public void setBrightnessConfigurationForDisplay(BrightnessConfiguration c,
                 String uniqueId, int userId, String packageName) {
@@ -3859,7 +3988,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
         @Override // Binder call
         public BrightnessConfiguration getBrightnessConfigurationForDisplay(String uniqueId,
                 int userId) {
@@ -3907,7 +4036,7 @@
 
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
+        @EnforcePermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
         @Override // Binder call
         public BrightnessConfiguration getDefaultBrightnessConfiguration() {
             getDefaultBrightnessConfiguration_enforcePermission();
@@ -3922,7 +4051,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+        @EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
         @Override
         public BrightnessInfo getBrightnessInfo(int displayId) {
             getBrightnessInfo_enforcePermission();
@@ -3953,7 +4082,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+        @EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
         @Override // Binder call
         public void setTemporaryBrightness(int displayId, float brightness) {
             setTemporaryBrightness_enforcePermission();
@@ -3968,7 +4097,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+        @EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
         @Override // Binder call
         public void setBrightness(int displayId, float brightness) {
             setBrightness_enforcePermission();
@@ -4010,7 +4139,7 @@
             return brightness;
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+        @EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
         @Override // Binder call
         public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
             setTemporaryAutoBrightnessAdjustment_enforcePermission();
@@ -4029,8 +4158,8 @@
         public void onShellCommand(FileDescriptor in, FileDescriptor out,
                 FileDescriptor err, String[] args, ShellCallback callback,
                 ResultReceiver resultReceiver) {
-            new DisplayManagerShellCommand(DisplayManagerService.this).exec(this, in, out, err,
-                    args, callback, resultReceiver);
+            new DisplayManagerShellCommand(DisplayManagerService.this, mFlags).exec(this, in, out,
+                    err, args, callback, resultReceiver);
         }
 
         @Override // Binder call
@@ -4053,7 +4182,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE)
+        @EnforcePermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE)
         @Override // Binder call
         public void setUserPreferredDisplayMode(int displayId, Display.Mode mode) {
             setUserPreferredDisplayMode_enforcePermission();
@@ -4141,7 +4270,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS)
+        @EnforcePermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS)
         @Override // Binder call
         public void setShouldAlwaysRespectAppRequestedMode(boolean enabled) {
             setShouldAlwaysRespectAppRequestedMode_enforcePermission();
@@ -4153,7 +4282,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS)
+        @EnforcePermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS)
         @Override // Binder call
         public boolean shouldAlwaysRespectAppRequestedMode() {
             shouldAlwaysRespectAppRequestedMode_enforcePermission();
@@ -4165,7 +4294,7 @@
             }
         }
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE)
+        @EnforcePermission(android.Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE)
         @Override // Binder call
         public void setRefreshRateSwitchingType(int newValue) {
             setRefreshRateSwitchingType_enforcePermission();
@@ -4217,6 +4346,28 @@
                 Binder.restoreCallingIdentity(token);
             }
         }
+
+        @EnforcePermission(MANAGE_DISPLAYS)
+        public void enableConnectedDisplay(int displayId) {
+            enableConnectedDisplay_enforcePermission();
+            if (!mFlags.isConnectedDisplayManagementEnabled()) {
+                Slog.w(TAG, "External display management is not enabled on your device: "
+                                    + "cannot enable display.");
+                return;
+            }
+            DisplayManagerService.this.enableConnectedDisplay(displayId, true);
+        }
+
+        @EnforcePermission(MANAGE_DISPLAYS)
+        public void disableConnectedDisplay(int displayId) {
+            disableConnectedDisplay_enforcePermission();
+            if (!mFlags.isConnectedDisplayManagementEnabled()) {
+                Slog.w(TAG, "External display management is not enabled on your device: "
+                                    + "cannot disable display.");
+                return;
+            }
+            DisplayManagerService.this.enableConnectedDisplay(displayId, false);
+        }
     }
 
     private static boolean isValidBrightness(float brightness) {
diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
index a4f4954..9b022d8 100644
--- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
+++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
@@ -23,6 +23,8 @@
 import android.util.Slog;
 import android.view.Display;
 
+import com.android.server.display.feature.DisplayManagerFlags;
+
 import java.io.PrintWriter;
 import java.util.Arrays;
 
@@ -30,9 +32,11 @@
     private static final String TAG = "DisplayManagerShellCommand";
 
     private final DisplayManagerService mService;
+    private final DisplayManagerFlags mFlags;
 
-    DisplayManagerShellCommand(DisplayManagerService service) {
+    DisplayManagerShellCommand(DisplayManagerService service, DisplayManagerFlags flags) {
         mService = service;
+        mFlags = flags;
     }
 
     @Override
@@ -82,6 +86,10 @@
                 return setDockedAndIdle();
             case "undock":
                 return unsetDockedAndIdle();
+            case "enable-display":
+                return setDisplayEnabled(true);
+            case "disable-display":
+                return setDisplayEnabled(false);
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -142,6 +150,12 @@
         pw.println("    Sets brightness to docked + idle screen brightness mode");
         pw.println("  undock");
         pw.println("    Sets brightness to active (normal) screen brightness mode");
+        if (mFlags.isConnectedDisplayManagementEnabled()) {
+            pw.println("  enable-display DISPLAY_ID");
+            pw.println("    Enable the DISPLAY_ID. Only possible if this is a connected display.");
+            pw.println("  disable-display DISPLAY_ID");
+            pw.println("    Disable the DISPLAY_ID. Only possible if this is a connected display.");
+        }
         pw.println();
         Intent.printIntentArgsHelp(pw , "");
     }
@@ -423,4 +437,26 @@
         mService.setDockedAndIdleEnabled(false, Display.DEFAULT_DISPLAY);
         return 0;
     }
+
+    private int setDisplayEnabled(boolean enable) {
+        if (!mFlags.isConnectedDisplayManagementEnabled()) {
+            getErrPrintWriter()
+                    .println("Error: external display management is not available on this device.");
+            return 1;
+        }
+        final String displayIdText = getNextArg();
+        if (displayIdText == null) {
+            getErrPrintWriter().println("Error: no displayId specified");
+            return 1;
+        }
+        final int displayId;
+        try {
+            displayId = Integer.parseInt(displayIdText);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: invalid displayId: '" + displayIdText + "'");
+            return 1;
+        }
+        mService.enableConnectedDisplay(displayId, enable);
+        return 0;
+    }
 }
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index b00b7a1..924b1b3 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -83,14 +83,14 @@
     private Context mOverlayContext;
 
     // Called with SyncRoot lock held.
-    public LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot,
-            Context context, Handler handler, Listener listener) {
+    LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
+            Handler handler, Listener listener) {
         this(syncRoot, context, handler, listener, new Injector());
     }
 
     @VisibleForTesting
-    LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot,
-            Context context, Handler handler, Listener listener, Injector injector) {
+    LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context, Handler handler,
+            Listener listener, Injector injector) {
         super(syncRoot, context, handler, listener, TAG);
         mInjector = injector;
         mSurfaceControlProxy = mInjector.getSurfaceControlProxy();
@@ -231,7 +231,6 @@
         private SurfaceControl.DisplayMode mActiveSfDisplayMode;
         // The active display vsync period in SurfaceFlinger
         private float mActiveRenderFrameRate;
-
         private DisplayEventReceiver.FrameRateOverride[] mFrameRateOverrides =
                 new DisplayEventReceiver.FrameRateOverride[0];
 
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 9c271ff5..0405ebe 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -193,7 +193,7 @@
     private SparseArray<SurfaceControl.RefreshRateRange> mThermalRefreshRateThrottling =
             new SparseArray<>();
 
-    public LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) {
+    LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) {
         mDisplayId = displayId;
         mLayerStack = layerStack;
         mPrimaryDisplayDevice = primaryDisplayDevice;
@@ -872,7 +872,10 @@
      * @param enabled True if enabled, false otherwise.
      */
     public void setEnabledLocked(boolean enabled) {
-        mIsEnabled = enabled;
+        if (enabled != mIsEnabled) {
+            mDirty = true;
+            mIsEnabled = enabled;
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 26f8029..cbe0fc7 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -40,6 +40,7 @@
 import android.view.DisplayInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
 import com.android.server.utils.FoldSettingWrapper;
@@ -71,6 +72,8 @@
     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 LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED = 7;
+    public static final int LOGICAL_DISPLAY_EVENT_CONNECTED = 8;
+    public static final int LOGICAL_DISPLAY_EVENT_DISCONNECTED = 9;
 
     public static final int DISPLAY_GROUP_EVENT_ADDED = 1;
     public static final int DISPLAY_GROUP_EVENT_CHANGED = 2;
@@ -124,6 +127,9 @@
     private final SparseArray<LogicalDisplay> mLogicalDisplays =
             new SparseArray<LogicalDisplay>();
 
+    // Cache whether or not the display was enabled on the last update.
+    private final SparseBooleanArray mDisplaysEnabledCache = new SparseBooleanArray();
+
     /** Map of all display groups indexed by display group id. */
     private final SparseArray<DisplayGroup> mDisplayGroups = new SparseArray<>();
 
@@ -188,19 +194,21 @@
     private int mDeviceStateToBeAppliedAfterBoot = DeviceStateManager.INVALID_DEVICE_STATE;
     private boolean mBootCompleted = false;
     private boolean mInteractive;
+    private final DisplayManagerFlags mFlags;
 
     LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo,
             @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
-            @NonNull Handler handler, FoldSettingWrapper foldSettingWrapper) {
-        this(context, repo, listener, syncRoot, handler,
-                new DeviceStateToLayoutMap((isDefault) -> isDefault ? DEFAULT_DISPLAY
-                        : sNextNonDefaultDisplayId++), foldSettingWrapper);
+            @NonNull Handler handler, FoldSettingWrapper foldSettingWrapper,
+            DisplayManagerFlags flags) {
+        this(context, repo, listener, syncRoot, handler, new DeviceStateToLayoutMap(
+                (isDefault) -> isDefault ? DEFAULT_DISPLAY : sNextNonDefaultDisplayId++),
+                foldSettingWrapper, flags);
     }
 
     LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo,
             @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
             @NonNull Handler handler, @NonNull DeviceStateToLayoutMap deviceStateToLayoutMap,
-            FoldSettingWrapper foldSettingWrapper) {
+            FoldSettingWrapper foldSettingWrapper, DisplayManagerFlags flags) {
         mSyncRoot = syncRoot;
         mPowerManager = context.getSystemService(PowerManager.class);
         mInteractive = mPowerManager.isInteractive();
@@ -217,6 +225,7 @@
                 com.android.internal.R.array.config_deviceStatesOnWhichToSleep));
         mDisplayDeviceRepo.addListener(this);
         mDeviceStateToLayoutMap = deviceStateToLayoutMap;
+        mFlags = flags;
     }
 
     @Override
@@ -664,10 +673,21 @@
         }
     }
 
-    private void updateLogicalDisplaysLocked() {
+    @VisibleForTesting
+    void updateLogicalDisplays() {
+        synchronized (mSyncRoot) {
+            updateLogicalDisplaysLocked();
+        }
+    }
+
+    void updateLogicalDisplaysLocked() {
         updateLogicalDisplaysLocked(DisplayDeviceInfo.DIFF_EVERYTHING);
     }
 
+    private void updateLogicalDisplaysLocked(int diff) {
+        updateLogicalDisplaysLocked(diff, /* isSecondLoop= */ false);
+    }
+
     /**
      * Updates the rest of the display system once all the changes are applied for display
      * devices and logical displays. The includes releasing invalid/empty LogicalDisplays,
@@ -676,8 +696,10 @@
      *
      * @param diff The DisplayDeviceInfo.DIFF_* of what actually changed to enable finer-grained
      *             display update listeners
+     * @param isSecondLoop If true, this is the second time this is called for the same change.
      */
-    private void updateLogicalDisplaysLocked(int diff) {
+    private void updateLogicalDisplaysLocked(int diff, boolean isSecondLoop) {
+        boolean reloop = false;
         // Go through all the displays and figure out if they need to be updated.
         // Loops in reverse so that displays can be removed during the loop without affecting the
         // rest of the loop.
@@ -694,11 +716,11 @@
             final DisplayInfo newDisplayInfo = display.getDisplayInfoLocked();
             final int updateState = mUpdatedLogicalDisplays.get(displayId, UPDATE_STATE_NEW);
             final boolean wasPreviouslyUpdated = updateState != UPDATE_STATE_NEW;
+            final boolean wasPreviouslyEnabled = mDisplaysEnabledCache.get(displayId);
+            final boolean isCurrentlyEnabled = display.isEnabledLocked();
 
             // The display is no longer valid and needs to be removed.
             if (!display.isValidLocked()) {
-                mUpdatedLogicalDisplays.delete(displayId);
-
                 // Remove from group
                 final DisplayGroup displayGroup = getDisplayGroupLocked(
                         getDisplayGroupIdFromDisplayIdLocked(displayId));
@@ -709,8 +731,20 @@
                 if (wasPreviouslyUpdated) {
                     // The display isn't actually removed from our internal data structures until
                     // after the notification is sent; see {@link #sendUpdatesForDisplaysLocked}.
-                    Slog.i(TAG, "Removing display: " + displayId);
-                    mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_REMOVED);
+                    if (mFlags.isConnectedDisplayManagementEnabled()) {
+                        if (mDisplaysEnabledCache.get(displayId)) {
+                            // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED
+                            reloop = true;
+                            mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_REMOVED);
+                        } else {
+                            mUpdatedLogicalDisplays.delete(displayId);
+                            mLogicalDisplaysToUpdate.put(displayId,
+                                    LOGICAL_DISPLAY_EVENT_DISCONNECTED);
+                        }
+                    } else {
+                        mUpdatedLogicalDisplays.delete(displayId);
+                        mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_REMOVED);
+                    }
                 } else {
                     // This display never left this class, safe to remove without notification
                     mLogicalDisplays.removeAt(i);
@@ -719,14 +753,23 @@
 
             // The display is new.
             } else if (!wasPreviouslyUpdated) {
-                Slog.i(TAG, "Adding new display: " + displayId + ": " + newDisplayInfo);
-                mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_ADDED);
-
+                if (mFlags.isConnectedDisplayManagementEnabled()) {
+                    // We still need to send LOGICAL_DISPLAY_EVENT_ADDED
+                    reloop = true;
+                    mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_CONNECTED);
+                } else {
+                    mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_ADDED);
+                }
             // Underlying displays device has changed to a different one.
             } else if (!TextUtils.equals(mTempDisplayInfo.uniqueId, newDisplayInfo.uniqueId)) {
                 mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_SWAPPED);
 
             // Something about the display device has changed.
+            } else if (mFlags.isConnectedDisplayManagementEnabled()
+                    && wasPreviouslyEnabled != isCurrentlyEnabled) {
+                int event = isCurrentlyEnabled ? LOGICAL_DISPLAY_EVENT_ADDED :
+                        LOGICAL_DISPLAY_EVENT_REMOVED;
+                mLogicalDisplaysToUpdate.put(displayId, event);
             } else if (wasDirty || !mTempDisplayInfo.equals(newDisplayInfo)) {
                 // If only the hdr/sdr ratio changed, then send just the event for that case
                 if ((diff == DisplayDeviceInfo.DIFF_HDR_SDR_RATIO)) {
@@ -789,9 +832,15 @@
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION);
         sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_ADDED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REMOVED);
+        if (mFlags.isConnectedDisplayManagementEnabled()) {
+            sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED);
+        }
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED);
+        if (mFlags.isConnectedDisplayManagementEnabled()) {
+            sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED);
+        }
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_ADDED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED);
         sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_CHANGED);
@@ -799,6 +848,14 @@
 
         mLogicalDisplaysToUpdate.clear();
         mDisplayGroupsToUpdate.clear();
+
+        if (reloop) {
+            if (isSecondLoop) {
+                Slog.wtf(TAG, "Trying to loop a third time");
+                return;
+            }
+            updateLogicalDisplaysLocked(diff, /* isSecondLoop= */ true);
+        }
     }
 
     /**
@@ -819,8 +876,22 @@
                 Slog.d(TAG, "Sending " + displayEventToString(msg) + " for display=" + id
                         + " with device=" + uniqueId);
             }
+
+            if (mFlags.isConnectedDisplayManagementEnabled()) {
+                if (msg == LOGICAL_DISPLAY_EVENT_ADDED) {
+                    mDisplaysEnabledCache.put(id, true);
+                } else if (msg == LOGICAL_DISPLAY_EVENT_REMOVED) {
+                    mDisplaysEnabledCache.delete(id);
+                }
+            }
+
             mListener.onLogicalDisplayEventLocked(display, msg);
-            if (msg == LOGICAL_DISPLAY_EVENT_REMOVED) {
+
+            if (mFlags.isConnectedDisplayManagementEnabled()) {
+                if (msg == LOGICAL_DISPLAY_EVENT_DISCONNECTED) {
+                    mLogicalDisplays.delete(id);
+                }
+            } else if (msg == LOGICAL_DISPLAY_EVENT_REMOVED) {
                 // We wait until we sent the EVENT_REMOVED event before actually removing the
                 // display.
                 mLogicalDisplays.delete(id);
@@ -1083,7 +1154,8 @@
         return display;
     }
 
-    private void setEnabledLocked(LogicalDisplay display, boolean isEnabled) {
+    @VisibleForTesting
+    void setEnabledLocked(LogicalDisplay display, boolean isEnabled) {
         final int displayId = display.getDisplayIdLocked();
         final DisplayInfo info = display.getDisplayInfoLocked();
 
@@ -1165,10 +1237,30 @@
                 return "removed";
             case LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED:
                 return "hdr_sdr_ratio_changed";
+            case LOGICAL_DISPLAY_EVENT_CONNECTED:
+                return "connected";
+            case LOGICAL_DISPLAY_EVENT_DISCONNECTED:
+                return "disconnected";
         }
         return null;
     }
 
+    void setDisplayEnabledLocked(int displayId, boolean enabled) {
+        LogicalDisplay display = getDisplayLocked(displayId);
+        if (display == null) {
+            Slog.w(TAG, "Cannot find display " + displayId);
+            return;
+        }
+        boolean isEnabled = display.isEnabledLocked();
+        if (isEnabled == enabled) {
+            Slog.w(TAG, "Display is already " + (isEnabled ? "enabled" : "disabled") + ": "
+                    + displayId);
+            return;
+        }
+        setEnabledLocked(display, enabled);
+        updateLogicalDisplaysLocked();
+    }
+
     public interface Listener {
         void onLogicalDisplayEventLocked(LogicalDisplay display, int event);
         void onDisplayGroupEventLocked(int groupId, int event);
diff --git a/services/core/java/com/android/server/display/feature/Android.bp b/services/core/java/com/android/server/display/feature/Android.bp
new file mode 100644
index 0000000..27c48ed
--- /dev/null
+++ b/services/core/java/com/android/server/display/feature/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+    name: "display_flags",
+    package: "com.android.server.display.feature.flags",
+    srcs: [
+        "*.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "display_flags_lib",
+    aconfig_declarations: "display_flags",
+}
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
new file mode 100644
index 0000000..78c2e9f
--- /dev/null
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.display.feature;
+
+import android.util.Slog;
+
+import com.android.server.display.feature.flags.Flags;
+
+/**
+ * Utility class to read the flags used in the display manager server.
+ */
+public class DisplayManagerFlags {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "DisplayManagerFlags";
+    private static final boolean DEFAULT_IS_CONNECTED_DISPLAY_MANAGEMENT_ENABLED = false;
+    private boolean mIsConnectedDisplayManagementEnabled = false;
+    private boolean mIsConnectedDisplayManagementEnabledSet = false;
+
+    // TODO(b/297159910): Simplify using READ-ONLY flags when available.
+    /** Returns whether connected display management is enabled or not. */
+    public boolean isConnectedDisplayManagementEnabled() {
+        if (mIsConnectedDisplayManagementEnabledSet) {
+            if (DEBUG) {
+                Slog.d(TAG, "isConnectedDisplayManagementEnabled. Recall = "
+                        + mIsConnectedDisplayManagementEnabled);
+            }
+            return mIsConnectedDisplayManagementEnabled;
+        }
+        try {
+            mIsConnectedDisplayManagementEnabled = Flags.enableConnectedDisplayManagement();
+            if (DEBUG) {
+                Slog.d(TAG, "isConnectedDisplayManagementEnabled. Flag value = "
+                        + mIsConnectedDisplayManagementEnabled);
+            }
+        } catch (Throwable ex) {
+            if (DEBUG) {
+                Slog.i(TAG, "isConnectedDisplayManagementEnabled not available: set to "
+                        + DEFAULT_IS_CONNECTED_DISPLAY_MANAGEMENT_ENABLED, ex);
+            }
+            mIsConnectedDisplayManagementEnabled = DEFAULT_IS_CONNECTED_DISPLAY_MANAGEMENT_ENABLED;
+        }
+        mIsConnectedDisplayManagementEnabledSet = true;
+        return mIsConnectedDisplayManagementEnabled;
+    }
+}
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
new file mode 100644
index 0000000..4d8c02b
--- /dev/null
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.display.feature.flags"
+
+# Important: Flags must be accessed through DisplayManagerFlags.
+
+flag {
+    name: "enable_connected_display_management"
+    namespace: "display_manager"
+    description: "Feature flag for Connected Display managment"
+    bug: "280739508"
+}
diff --git a/services/tests/displayservicetests/Android.bp b/services/tests/displayservicetests/Android.bp
index 4f555d9..fb14419 100644
--- a/services/tests/displayservicetests/Android.bp
+++ b/services/tests/displayservicetests/Android.bp
@@ -28,6 +28,7 @@
 
     static_libs: [
         "androidx.test.ext.junit",
+        "androidx.test.rules",
         "frameworks-base-testutils",
         "junit",
         "junit-params",
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 979676e..d099693 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
+import static android.Manifest.permission.MANAGE_DISPLAYS;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
@@ -42,7 +43,9 @@
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -104,11 +107,14 @@
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.display.DisplayManagerService.DeviceStateListener;
 import com.android.server.display.DisplayManagerService.SyncRoot;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.lights.LightsManager;
 import com.android.server.sensors.SensorManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
+import com.google.common.truth.Expect;
+
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
@@ -125,6 +131,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -133,6 +140,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.stream.LongStream;
 
+// TODO(b/297170420) Parameterize the test.
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class DisplayManagerServiceTest {
@@ -146,12 +154,31 @@
     private static final long STANDARD_DISPLAY_EVENTS = DisplayManager.EVENT_FLAG_DISPLAY_ADDED
                     | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
                     | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
+    private static final long STANDARD_AND_CONNECTION_DISPLAY_EVENTS =
+            STANDARD_DISPLAY_EVENTS | DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
 
-    @Rule
+    private static final String EVENT_DISPLAY_ADDED = "EVENT_DISPLAY_ADDED";
+    private static final String EVENT_DISPLAY_REMOVED = "EVENT_DISPLAY_REMOVED";
+    private static final String EVENT_DISPLAY_CHANGED = "EVENT_DISPLAY_CHANGED";
+    private static final String EVENT_DISPLAY_BRIGHTNESS_CHANGED =
+            "EVENT_DISPLAY_BRIGHTNESS_CHANGED";
+    private static final String EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED =
+            "EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED";
+    private static final String EVENT_DISPLAY_CONNECTED = "EVENT_DISPLAY_CONNECTED";
+    private static final String EVENT_DISPLAY_DISCONNECTED = "EVENT_DISPLAY_DISCONNECTED";
+    private static final String DISPLAY_GROUP_EVENT_ADDED = "DISPLAY_GROUP_EVENT_ADDED";
+    private static final String DISPLAY_GROUP_EVENT_REMOVED = "DISPLAY_GROUP_EVENT_REMOVED";
+    private static final String DISPLAY_GROUP_EVENT_CHANGED = "DISPLAY_GROUP_EVENT_CHANGED";
+
+    @Rule(order = 0)
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
+    @Rule(order = 1)
+    public Expect expect = Expect.create();
 
     private Context mContext;
 
+    private Resources mResources;
+
     private int mHdrConversionMode;
 
     private int mPreferredHdrOutputType;
@@ -189,6 +216,11 @@
         }
 
         @Override
+        DisplayManagerFlags getFlags() {
+            return mMockFlags;
+        }
+
+        @Override
         VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot, Context context,
                 Handler handler, DisplayAdapter.Listener displayAdapterListener) {
             return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
@@ -208,8 +240,8 @@
         @Override
         LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
                 Handler handler, DisplayAdapter.Listener displayAdapterListener) {
-            return new LocalDisplayAdapter(syncRoot, context, handler,
-                    displayAdapterListener, new LocalDisplayAdapter.Injector() {
+            return new LocalDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
+                    new LocalDisplayAdapter.Injector() {
                         @Override
                         public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
                             return mSurfaceControlProxy;
@@ -262,10 +294,12 @@
     @Mock DisplayDeviceConfig mMockDisplayDeviceConfig;
 
     @Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor;
+    @Mock DisplayManagerFlags mMockFlags;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false);
 
         LocalServices.removeServiceForTest(InputManagerInternal.class);
         LocalServices.addService(InputManagerInternal.class, mMockInputManagerInternal);
@@ -280,6 +314,9 @@
                 VirtualDeviceManagerInternal.class, mMockVirtualDeviceManagerInternal);
         // TODO: b/287945043
         mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
+        mResources = Mockito.spy(mContext.getResources());
+        manageDisplaysPermission(/* granted= */ false);
+        when(mContext.getResources()).thenReturn(mResources);
 
         VirtualDeviceManager vdm = new VirtualDeviceManager(mIVirtualDeviceManager, mContext);
         when(mContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
@@ -315,7 +352,7 @@
         // This is to update the display device config such that DisplayManagerService can ignore
         // the usage of SensorManager, which is available only after the PowerManagerService
         // is ready.
-        resetConfigToIgnoreSensorManager(mContext);
+        resetConfigToIgnoreSensorManager();
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         registerDefaultDisplays(displayManager);
         displayManager.systemReady(false /* safeMode */);
@@ -392,7 +429,7 @@
         // This is to update the display device config such that DisplayManagerService can ignore
         // the usage of SensorManager, which is available only after the PowerManagerService
         // is ready.
-        resetConfigToIgnoreSensorManager(mContext);
+        resetConfigToIgnoreSensorManager();
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         registerDefaultDisplays(displayManager);
         displayManager.systemReady(false /* safeMode */);
@@ -670,7 +707,7 @@
 
         Handler handler = displayManager.getDisplayHandler();
         waitForIdleHandler(handler);
-        assertTrue(callback.mDisplayChangedCalled);
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_CHANGED);
     }
 
     /**
@@ -696,7 +733,7 @@
      * Tests that we get a Runtime exception when we cannot initialize the virtual display.
      */
     @Test
-    public void testStartVirtualDisplayWithDefDisplay_NoVirtualDisplayAdapter() throws Exception {
+    public void testStartVirtualDisplayWithDefDisplay_NoVirtualDisplayAdapter() {
         DisplayManagerService displayManager = new DisplayManagerService(mContext,
                 new DisplayManagerService.Injector() {
                     @Override
@@ -1487,7 +1524,7 @@
      * list is updated.
      */
     @Test
-    public void testShouldNotifyChangeWhenDisplayInfoFrameRateOverrideChanged() throws Exception {
+    public void testShouldNotifyChangeWhenDisplayInfoFrameRateOverrideChanged() {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mShortMockedInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
@@ -1504,7 +1541,8 @@
                 new DisplayEventReceiver.FrameRateOverride[]{
                         new DisplayEventReceiver.FrameRateOverride(myUid, 30f),
                 });
-        assertTrue(callback.mDisplayChangedCalled);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED);
         callback.clear();
 
         updateFrameRateOverride(displayManager, displayDevice,
@@ -1512,7 +1550,8 @@
                         new DisplayEventReceiver.FrameRateOverride(myUid, 30f),
                         new DisplayEventReceiver.FrameRateOverride(1234, 30f),
                 });
-        assertFalse(callback.mDisplayChangedCalled);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED);
 
         updateFrameRateOverride(displayManager, displayDevice,
                 new DisplayEventReceiver.FrameRateOverride[]{
@@ -1520,7 +1559,8 @@
                         new DisplayEventReceiver.FrameRateOverride(1234, 30f),
                         new DisplayEventReceiver.FrameRateOverride(5678, 30f),
                 });
-        assertTrue(callback.mDisplayChangedCalled);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED);
         callback.clear();
 
         updateFrameRateOverride(displayManager, displayDevice,
@@ -1528,21 +1568,23 @@
                         new DisplayEventReceiver.FrameRateOverride(1234, 30f),
                         new DisplayEventReceiver.FrameRateOverride(5678, 30f),
                 });
-        assertTrue(callback.mDisplayChangedCalled);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED);
         callback.clear();
 
         updateFrameRateOverride(displayManager, displayDevice,
                 new DisplayEventReceiver.FrameRateOverride[]{
                         new DisplayEventReceiver.FrameRateOverride(5678, 30f),
                 });
-        assertFalse(callback.mDisplayChangedCalled);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED);
     }
 
     /**
      * Tests that the DisplayInfo is updated correctly with a frame rate override
      */
     @Test
-    public void testDisplayInfoFrameRateOverride() throws Exception {
+    public void testDisplayInfoFrameRateOverride() {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mShortMockedInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
@@ -1579,7 +1621,7 @@
      * DisplayInfo#getRefreshRate
      */
     @Test
-    public void testDisplayInfoNonNativeFrameRateOverride() throws Exception {
+    public void testDisplayInfoNonNativeFrameRateOverride() {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
@@ -1627,7 +1669,7 @@
      */
     @Test
     @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
-    public void testDisplayInfoNonNativeFrameRateOverrideModeCompat() throws Exception {
+    public void testDisplayInfoNonNativeFrameRateOverrideModeCompat() {
         testDisplayInfoNonNativeFrameRateOverrideMode(/*compatChangeEnabled*/ false);
     }
 
@@ -1636,7 +1678,7 @@
      */
     @Test
     @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
-    public void testDisplayInfoNonNativeFrameRateOverrideMode() throws Exception {
+    public void testDisplayInfoNonNativeFrameRateOverrideMode() {
         testDisplayInfoNonNativeFrameRateOverrideMode(/*compatChangeEnabled*/ true);
     }
 
@@ -1644,7 +1686,7 @@
      * Tests that there is a display change notification if the render frame rate is updated
      */
     @Test
-    public void testShouldNotifyChangeWhenDisplayInfoRenderFrameRateChanged() throws Exception {
+    public void testShouldNotifyChangeWhenDisplayInfoRenderFrameRateChanged() {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mShortMockedInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
@@ -1657,14 +1699,17 @@
                 displayManagerBinderService, displayDevice);
 
         updateRenderFrameRate(displayManager, displayDevice, 30f);
-        assertTrue(callback.mDisplayChangedCalled);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED);
         callback.clear();
 
         updateRenderFrameRate(displayManager, displayDevice, 30f);
-        assertFalse(callback.mDisplayChangedCalled);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED);
 
         updateRenderFrameRate(displayManager, displayDevice, 20f);
-        assertTrue(callback.mDisplayChangedCalled);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED);
         callback.clear();
     }
 
@@ -1672,7 +1717,7 @@
      * Tests that the DisplayInfo is updated correctly with a render frame rate
      */
     @Test
-    public void testDisplayInfoRenderFrameRate() throws Exception {
+    public void testDisplayInfoRenderFrameRate() {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mShortMockedInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
@@ -1734,9 +1779,7 @@
 
         waitForIdleHandler(handler);
 
-        assertFalse(callback.mDisplayChangedCalled);
-        assertFalse(callback.mDisplayRemovedCalled);
-        assertTrue(callback.mDisplayAddedCalled);
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_ADDED);
     }
 
     /**
@@ -1766,9 +1809,7 @@
 
         waitForIdleHandler(handler);
 
-        assertFalse(callback.mDisplayChangedCalled);
-        assertFalse(callback.mDisplayRemovedCalled);
-        assertFalse(callback.mDisplayAddedCalled);
+        assertThat(callback.receivedEvents()).isEmpty();
     }
 
     /**
@@ -1780,12 +1821,15 @@
                 new DisplayManagerService(mContext, mShortMockedInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
                 displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
 
         Handler handler = displayManager.getDisplayHandler();
         waitForIdleHandler(handler);
 
         FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
                 new float[]{60f});
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
 
         waitForIdleHandler(handler);
 
@@ -1794,14 +1838,12 @@
 
         waitForIdleHandler(handler);
 
+        display.setPrimaryDisplayDeviceLocked(null);
         displayManager.getDisplayDeviceRepository()
                 .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
-
         waitForIdleHandler(handler);
 
-        assertFalse(callback.mDisplayChangedCalled);
-        assertTrue(callback.mDisplayRemovedCalled);
-        assertFalse(callback.mDisplayAddedCalled);
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_REMOVED);
     }
 
     /**
@@ -1814,12 +1856,15 @@
                 new DisplayManagerService(mContext, mShortMockedInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
                 displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
 
         Handler handler = displayManager.getDisplayHandler();
         waitForIdleHandler(handler);
 
         FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
                 new float[]{60f});
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
 
         waitForIdleHandler(handler);
 
@@ -1831,14 +1876,12 @@
 
         waitForIdleHandler(handler);
 
+        display.setPrimaryDisplayDeviceLocked(null);
         displayManager.getDisplayDeviceRepository()
                 .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
-
         waitForIdleHandler(handler);
 
-        assertFalse(callback.mDisplayChangedCalled);
-        assertFalse(callback.mDisplayRemovedCalled);
-        assertFalse(callback.mDisplayAddedCalled);
+        assertThat(callback.receivedEvents()).isEmpty();
     }
 
 
@@ -2052,8 +2095,315 @@
         assertNull(result);
     }
 
-    private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled)
-            throws Exception {
+    @Test
+    public void testConnectExternalDisplay_withoutDisplayManagement_shouldAddDisplay() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false);
+        manageDisplaysPermission(/* granted= */ true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        assertThat(display.isEnabledLocked()).isTrue();
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_ADDED);
+
+    }
+
+    @Test
+    public void testConnectExternalDisplay_withDisplayManagement_shouldDisableDisplay() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        manageDisplaysPermission(/* granted= */ true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        // Create default display device
+        createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+
+        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+        localService.registerDisplayGroupListener(callback);
+
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        assertThat(display.isEnabledLocked()).isFalse();
+        assertThat(callback.receivedEvents()).containsExactly(DISPLAY_GROUP_EVENT_ADDED,
+                EVENT_DISPLAY_CONNECTED).inOrder();
+    }
+
+    @Test
+    public void testConnectInternalDisplay_withDisplayManagement_shouldConnectAndAddDisplay() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        manageDisplaysPermission(/* granted= */ true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+
+        createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_CONNECTED,
+                EVENT_DISPLAY_ADDED).inOrder();
+    }
+
+    @Test
+    public void testEnableExternalDisplay_withDisplayManagement_shouldSignalDisplayAdded() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        manageDisplaysPermission(/* granted= */ true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        // Create default display device
+        createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        displayManager.enableConnectedDisplay(display.getDisplayIdLocked(), /* enabled= */ true);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        assertThat(display.isEnabledLocked()).isTrue();
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_ADDED).inOrder();
+    }
+
+    @Test
+    public void testEnableExternalDisplay_withoutPermission_shouldThrowException() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        // Create default display device
+        createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        int displayId = display.getDisplayIdLocked();
+
+        assertThrows(SecurityException.class, () -> bs.enableConnectedDisplay(displayId));
+    }
+
+    @Test
+    public void testEnableInternalDisplay_withManageDisplays_shouldSignalAdded() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ false);
+        logicalDisplayMapper.updateLogicalDisplays();
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+
+        logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ true);
+        logicalDisplayMapper.updateLogicalDisplays();
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_ADDED);
+    }
+
+    @Test
+    public void testDisableInternalDisplay_withDisplayManagement_shouldSignalRemove() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+
+        logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ false);
+        logicalDisplayMapper.updateLogicalDisplays();
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_REMOVED);
+    }
+
+    @Test
+    public void testDisableExternalDisplay_shouldSignalDisplayRemoved() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        // Create default display device
+        createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS);
+        localService.registerDisplayGroupListener(callback);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ true);
+        logicalDisplayMapper.updateLogicalDisplays();
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+
+        logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ false);
+        logicalDisplayMapper.updateLogicalDisplays();
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        assertThat(display.isEnabledLocked()).isFalse();
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_REMOVED);
+    }
+
+    @Test
+    public void testDisableExternalDisplay_withoutPermission_shouldThrowException() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        // Create default display device
+        createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        int displayId = display.getDisplayIdLocked();
+        logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ true);
+        logicalDisplayMapper.updateLogicalDisplays();
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+
+        assertThrows(SecurityException.class, () -> bs.disableConnectedDisplay(displayId));
+    }
+
+    @Test
+    public void testRemoveExternalDisplay_whenDisabled_shouldSignalDisconnected() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        manageDisplaysPermission(/* granted= */ true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        // Create default display device
+        createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+        localService.registerDisplayGroupListener(callback);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+        LogicalDisplay display = logicalDisplayMapper.getDisplayLocked(displayDevice);
+        int groupId = display.getDisplayInfoLocked().displayGroupId;
+        DisplayGroup group = logicalDisplayMapper.getDisplayGroupLocked(groupId);
+        assertThat(group.getSizeLocked()).isEqualTo(1);
+
+        display.setPrimaryDisplayDeviceLocked(null);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        assertThat(group.getSizeLocked()).isEqualTo(0);
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_DISCONNECTED,
+                DISPLAY_GROUP_EVENT_REMOVED);
+    }
+
+    @Test
+    public void testRegisterCallback_withoutPermission_shouldThrow() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+
+        assertThrows(SecurityException.class, () -> bs.registerCallbackWithEventMask(callback,
+                STANDARD_AND_CONNECTION_DISPLAY_EVENTS));
+    }
+
+    @Test
+    public void testRemoveExternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        manageDisplaysPermission(/* granted= */ true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        int displayId = display.getDisplayIdLocked();
+        displayManager.enableConnectedDisplay(displayId, /* enabled= */ true);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+
+        display.setPrimaryDisplayDeviceLocked(null);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        assertThat(logicalDisplayMapper.getDisplayLocked(displayId, true)).isNull();
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_REMOVED,
+                EVENT_DISPLAY_DISCONNECTED).inOrder();
+    }
+
+    @Test
+    public void testRemoveInternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() {
+        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        manageDisplaysPermission(/* granted= */ true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+        callback.clear();
+
+        display.setPrimaryDisplayDeviceLocked(null);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
+        waitForIdleHandler(displayManager.getDisplayHandler());
+
+        assertThat(logicalDisplayMapper.getDisplayLocked(displayDevice,
+                /* includeDisabled= */ true)).isNull();
+        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_REMOVED,
+                EVENT_DISPLAY_DISCONNECTED);
+    }
+    private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled) {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mShortMockedInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
@@ -2086,8 +2436,7 @@
         assertEquals(expectedMode, displayInfo.getMode());
     }
 
-    private void testDisplayInfoRenderFrameRateModeCompat(boolean compatChangeEnabled)
-            throws Exception {
+    private void testDisplayInfoRenderFrameRateModeCompat(boolean compatChangeEnabled)  {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mShortMockedInjector);
         DisplayManagerService.BinderService displayManagerBinderService =
@@ -2214,6 +2563,15 @@
             DisplayManagerService displayManager,
             DisplayManagerService.BinderService displayManagerBinderService,
             FakeDisplayDevice displayDevice) {
+        return registerDisplayListenerCallback(displayManager, displayManagerBinderService,
+                displayDevice, STANDARD_DISPLAY_EVENTS);
+    }
+
+    private FakeDisplayManagerCallback registerDisplayListenerCallback(
+            DisplayManagerService displayManager,
+            DisplayManagerService.BinderService displayManagerBinderService,
+            FakeDisplayDevice displayDevice,
+            long displayEventsMask) {
         // Find the display id of the added FakeDisplayDevice
         int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
                 displayDevice);
@@ -2224,12 +2582,18 @@
         // register display listener callback
         FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(displayId);
         displayManagerBinderService.registerCallbackWithEventMask(
-                callback, STANDARD_DISPLAY_EVENTS);
+                callback, displayEventsMask);
         return callback;
     }
 
     private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
-            float[] refreshRates) {
+                                                      float[] refreshRates) {
+        return createFakeDisplayDevice(displayManager, refreshRates, Display.TYPE_UNKNOWN);
+    }
+
+    private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
+                                                      float[] refreshRates,
+                                                      int displayType) {
         FakeDisplayDevice displayDevice = new FakeDisplayDevice();
         DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
         int width = 100;
@@ -2240,6 +2604,7 @@
                     new Display.Mode(i + 1, width, height, refreshRates[i]);
         }
         displayDeviceInfo.modeId = 1;
+        displayDeviceInfo.type = displayType;
         displayDeviceInfo.renderFrameRate = displayDeviceInfo.supportedModes[0].getRefreshRate();
         displayDeviceInfo.width = width;
         displayDeviceInfo.height = height;
@@ -2248,6 +2613,9 @@
                 Insets.of(0, 10, 0, 0),
                 zeroRect, new Rect(0, 0, 10, 10), zeroRect, zeroRect);
         displayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
+        if (displayType == Display.TYPE_EXTERNAL) {
+            displayDeviceInfo.flags |= DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP;
+        }
         displayDeviceInfo.address = new TestUtils.TestDisplayAddress();
         displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
         displayManager.getDisplayDeviceRepository()
@@ -2282,25 +2650,30 @@
         }
     }
 
-    private void resetConfigToIgnoreSensorManager(Context context) {
-        final Resources res = Mockito.spy(context.getResources());
-        doReturn(new int[]{-1}).when(res).getIntArray(R.array
+    private void resetConfigToIgnoreSensorManager() {
+        doReturn(new int[]{-1}).when(mResources).getIntArray(R.array
                 .config_ambientThresholdsOfPeakRefreshRate);
-        doReturn(new int[]{-1}).when(res).getIntArray(R.array
+        doReturn(new int[]{-1}).when(mResources).getIntArray(R.array
                 .config_brightnessThresholdsOfPeakRefreshRate);
-        doReturn(new int[]{-1}).when(res).getIntArray(R.array
+        doReturn(new int[]{-1}).when(mResources).getIntArray(R.array
                 .config_highDisplayBrightnessThresholdsOfFixedRefreshRate);
-        doReturn(new int[]{-1}).when(res).getIntArray(R.array
+        doReturn(new int[]{-1}).when(mResources).getIntArray(R.array
                 .config_highAmbientBrightnessThresholdsOfFixedRefreshRate);
-
-        when(context.getResources()).thenReturn(res);
     }
 
-    private class FakeDisplayManagerCallback extends IDisplayManagerCallback.Stub {
+    private void manageDisplaysPermission(boolean granted) {
+        if (granted) {
+            doNothing().when(mContext).enforceCallingOrSelfPermission(eq(MANAGE_DISPLAYS), any());
+        } else {
+            doThrow(new SecurityException("MANAGE_DISPLAYS permission denied")).when(mContext)
+                    .enforceCallingOrSelfPermission(eq(MANAGE_DISPLAYS), any());
+        }
+    }
+
+    private static class FakeDisplayManagerCallback extends IDisplayManagerCallback.Stub
+            implements DisplayManagerInternal.DisplayGroupListener {
         int mDisplayId;
-        boolean mDisplayAddedCalled = false;
-        boolean mDisplayChangedCalled = false;
-        boolean mDisplayRemovedCalled = false;
+        List<String> mReceivedEvents = new ArrayList<>();
 
         FakeDisplayManagerCallback(int displayId) {
             mDisplayId = displayId;
@@ -2316,23 +2689,55 @@
                 return;
             }
 
-            if (event == DisplayManagerGlobal.EVENT_DISPLAY_ADDED) {
-                mDisplayAddedCalled = true;
-            }
+            // We convert the event to a string for two reasons:
+            // 1 - The error produced is a lot easier to read
+            // 2 - The values used for display and group events are the same, strings are used to
+            // differentiate them easily.
+            mReceivedEvents.add(eventTypeToString(event));
+        }
 
-            if (event == DisplayManagerGlobal.EVENT_DISPLAY_CHANGED) {
-                mDisplayChangedCalled = true;
-            }
+        @Override
+        public void onDisplayGroupAdded(int groupId) {
+            mReceivedEvents.add(DISPLAY_GROUP_EVENT_ADDED);
+        }
 
-            if (event == DisplayManagerGlobal.EVENT_DISPLAY_REMOVED) {
-                mDisplayRemovedCalled = true;
-            }
+        @Override
+        public void onDisplayGroupRemoved(int groupId) {
+            mReceivedEvents.add(DISPLAY_GROUP_EVENT_REMOVED);
+        }
+
+        @Override
+        public void onDisplayGroupChanged(int groupId) {
+            mReceivedEvents.add(DISPLAY_GROUP_EVENT_CHANGED);
         }
 
         public void clear() {
-            mDisplayAddedCalled = false;
-            mDisplayChangedCalled = false;
-            mDisplayRemovedCalled = false;
+            mReceivedEvents.clear();
+        }
+
+        private String eventTypeToString(int eventType) {
+            switch (eventType) {
+                case DisplayManagerGlobal.EVENT_DISPLAY_ADDED:
+                    return EVENT_DISPLAY_ADDED;
+                case DisplayManagerGlobal.EVENT_DISPLAY_REMOVED:
+                    return EVENT_DISPLAY_REMOVED;
+                case DisplayManagerGlobal.EVENT_DISPLAY_CHANGED:
+                    return EVENT_DISPLAY_CHANGED;
+                case DisplayManagerGlobal.EVENT_DISPLAY_BRIGHTNESS_CHANGED:
+                    return EVENT_DISPLAY_BRIGHTNESS_CHANGED;
+                case DisplayManagerGlobal.EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED:
+                    return EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED;
+                case DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED:
+                    return EVENT_DISPLAY_CONNECTED;
+                case DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED:
+                    return EVENT_DISPLAY_DISCONNECTED;
+                default:
+                    return "UNKNOWN: " + eventType;
+            }
+        }
+
+        List<String> receivedEvents() {
+            return mReceivedEvents;
         }
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index aa0a2fe..6cde5e3 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -107,7 +107,6 @@
     private LightsManager mMockedLightsManager;
     @Mock
     private LogicalLight mMockedBacklight;
-
     private Handler mHandler;
 
     private TestListener mListener = new TestListener();
@@ -1063,6 +1062,53 @@
         assertThat(info.roundedCorners).isNull();
     }
 
+    @Test
+    public void test_createLocalExternalDisplay_displayManagementEnabled_shouldHaveDefaultGroup()
+            throws Exception {
+        FakeDisplay display = new FakeDisplay(PORT_A);
+        display.info.isInternal = false;
+        setUpDisplay(display);
+        updateAvailableDisplays();
+        mAdapter.registerLocked();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        DisplayDevice displayDevice = mListener.addedDisplays.get(0);
+
+        // Turn on / initialize
+        Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0,
+                0);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        mListener.changedDisplays.clear();
+
+        DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked();
+
+        assertThat(info.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP).isEqualTo(0);
+    }
+    @Test
+    public void test_createLocalExternalDisplay_displayManagementDisabled_shouldNotHaveOwnGroup()
+            throws Exception {
+        FakeDisplay display = new FakeDisplay(PORT_A);
+        display.info.isInternal = false;
+        setUpDisplay(display);
+        updateAvailableDisplays();
+        mAdapter.registerLocked();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        DisplayDevice displayDevice = mListener.addedDisplays.get(0);
+
+        // Turn on / initialize
+        Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0,
+                0);
+        changeStateRunnable.run();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        mListener.changedDisplays.clear();
+
+        DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked();
+
+        assertThat(info.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP).isEqualTo(0);
+    }
+
     private void setupCutoutAndRoundedCorners() {
         String sampleCutout = "M 507,66\n"
                 + "a 33,33 0 1 0 66,0 33,33 0 1 0 -66,0\n"
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 0fe6e64..6954435 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -30,6 +30,8 @@
 import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED;
 import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED;
+import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CONNECTED;
+import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED;
 import static com.android.server.display.layout.Layout.Display.POSITION_REAR;
 import static com.android.server.display.layout.Layout.Display.POSITION_UNKNOWN;
@@ -44,6 +46,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -63,6 +66,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
 import com.android.server.utils.FoldSettingWrapper;
@@ -79,7 +83,9 @@
 import java.io.File;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -107,8 +113,11 @@
     @Mock IThermalService mIThermalServiceMock;
     @Spy DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy =
             new DeviceStateToLayoutMap(mIdProducer, NON_EXISTING_FILE);
+    @Mock
+    DisplayManagerFlags mFlagsMock;
 
     @Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor;
+    @Captor ArgumentCaptor<Integer> mDisplayEventCaptor;
 
     @Before
     public void setUp() {
@@ -154,11 +163,12 @@
                 com.android.internal.R.array.config_deviceStatesOnWhichToSleep))
                 .thenReturn(new int[]{0});
 
+        when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(false);
         mLooper = new TestLooper();
         mHandler = new Handler(mLooper.getLooper());
         mLogicalDisplayMapper = new LogicalDisplayMapper(mContextMock, mDisplayDeviceRepo,
                 mListenerMock, new DisplayManagerService.SyncRoot(), mHandler,
-                mDeviceStateToLayoutMapSpy, mFoldSettingWrapperMock);
+                mDeviceStateToLayoutMapSpy, mFoldSettingWrapperMock, mFlagsMock);
     }
 
 
@@ -279,6 +289,67 @@
     }
 
     @Test
+    public void testDisplayDeviceAddAndRemove_withDisplayManagement() {
+        when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+
+        // add
+        mDisplayDeviceRepo.onDisplayDeviceEvent(device, DISPLAY_DEVICE_EVENT_ADDED);
+
+        verify(mListenerMock, times(2)).onLogicalDisplayEventLocked(
+                mDisplayCaptor.capture(), mDisplayEventCaptor.capture());
+        LogicalDisplay added = mDisplayCaptor.getAllValues().get(0);
+        assertThat(mDisplayCaptor.getAllValues().get(1)).isEqualTo(added);
+        LogicalDisplay displayAdded = add(device);
+        assertThat(info(displayAdded).address).isEqualTo(info(device).address);
+        assertThat(id(displayAdded)).isEqualTo(DEFAULT_DISPLAY);
+        assertThat(mDisplayEventCaptor.getAllValues()).containsExactly(
+                LOGICAL_DISPLAY_EVENT_CONNECTED, LOGICAL_DISPLAY_EVENT_ADDED).inOrder();
+        clearInvocations(mListenerMock);
+
+        // remove
+        mDisplayDeviceRepo.onDisplayDeviceEvent(device, DISPLAY_DEVICE_EVENT_REMOVED);
+        verify(mListenerMock, times(2)).onLogicalDisplayEventLocked(
+                mDisplayCaptor.capture(), mDisplayEventCaptor.capture());
+        List<Integer> allEvents = mDisplayEventCaptor.getAllValues();
+        int numEvents = allEvents.size();
+        // Only extract the last two events
+        List<Integer> events = new ArrayList(2);
+        events.add(allEvents.get(numEvents - 2));
+        events.add(allEvents.get(numEvents - 1));
+        assertThat(events).containsExactly(
+                LOGICAL_DISPLAY_EVENT_REMOVED, LOGICAL_DISPLAY_EVENT_DISCONNECTED).inOrder();
+        List<LogicalDisplay> displays = mDisplayCaptor.getAllValues();
+        LogicalDisplay displayRemoved = displays.get(numEvents - 2);
+        assertThat(displays.get(numEvents - 1)).isEqualTo(displayRemoved);
+        assertThat(id(displayRemoved)).isEqualTo(DEFAULT_DISPLAY);
+        assertThat(displayRemoved).isEqualTo(displayAdded);
+    }
+
+    @Test
+    public void testDisplayDisableEnable_withDisplayManagement() {
+        when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true);
+        DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        LogicalDisplay displayAdded = add(device);
+        assertThat(displayAdded.isEnabledLocked()).isTrue();
+
+        // Disable device
+        mLogicalDisplayMapper.setDisplayEnabledLocked(
+                displayAdded.getDisplayIdLocked(), /* isEnabled= */ false);
+        verify(mListenerMock).onLogicalDisplayEventLocked(mDisplayCaptor.capture(),
+                eq(LOGICAL_DISPLAY_EVENT_REMOVED));
+        clearInvocations(mListenerMock);
+
+        // Enable device
+        mLogicalDisplayMapper.setDisplayEnabledLocked(
+                displayAdded.getDisplayIdLocked(), /* isEnabled= */ true);
+        verify(mListenerMock).onLogicalDisplayEventLocked(mDisplayCaptor.capture(),
+                eq(LOGICAL_DISPLAY_EVENT_ADDED));
+    }
+
+    @Test
     public void testGetDisplayIdsLocked() {
         add(createDisplayDevice(TYPE_INTERNAL, 600, 800,
                 FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));