Loading core/java/android/hardware/display/DisplayTopology.java +14 −0 Original line number Diff line number Diff line Loading @@ -158,6 +158,20 @@ public final class DisplayTopology implements Parcelable { return mPrimaryDisplayId; } /** * @hide */ public boolean isEmpty() { return mRoot == null; } /** * @hide */ public boolean hasMultipleDisplays() { return mRoot != null && mRoot.mChildren != null && !mRoot.mChildren.isEmpty(); } /** * Add a display to the topology. * If this is the second display in the topology, it will be placed above the first display. Loading services/core/java/com/android/server/display/DisplayManagerService.java +71 −2 Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.Process.FIRST_APPLICATION_UID; import static android.os.Process.ROOT_UID; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; import static android.provider.Settings.Secure.INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY; import static android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY; import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL; import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH; Loading Loading @@ -195,6 +196,7 @@ import com.android.server.display.utils.DebugUtils; import com.android.server.display.utils.SensorUtils; import com.android.server.input.InputManagerInternal; import com.android.server.utils.FoldSettingProvider; import com.android.server.wm.DesktopModeHelper; import com.android.server.wm.SurfaceAnimationThread; import com.android.server.wm.WindowManagerInternal; Loading Loading @@ -550,6 +552,10 @@ public final class DisplayManagerService extends SystemService { private boolean mMirrorBuiltInDisplay; // Whether default display should be included in the display topology. Note that this should // only be used for the devices in projected mode. private boolean mIncludeDefaultDisplayInTopology; private final BroadcastReceiver mIdleModeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Loading Loading @@ -691,8 +697,9 @@ public final class DisplayManagerService extends SystemService { deliverTopologyUpdate(update.first); }; mDisplayTopologyCoordinator = new DisplayTopologyCoordinator( this::isExtendedDisplayAllowed, topologyChangedCallback, new HandlerExecutor(mHandler), mSyncRoot, backupManager::dataChanged); this::isExtendedDisplayAllowed, this::shouldIncludeDefaultDisplayInTopology, topologyChangedCallback, new HandlerExecutor(mHandler), mSyncRoot, backupManager::dataChanged, mFlags); } else { mDisplayTopologyCoordinator = null; } Loading Loading @@ -875,6 +882,12 @@ public final class DisplayManagerService extends SystemService { if (mFlags.isDisplayContentModeManagementEnabled()) { updateMirrorBuiltInDisplaySettingLocked(/*shouldSendDisplayChangeEvent=*/ false); } if (mFlags.isDefaultDisplayInTopologySwitchEnabled()) { mIncludeDefaultDisplayInTopology = mInjector.canInternalDisplayHostDesktops( mContext) || (Settings.Secure.getIntForUser(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0, UserHandle.USER_CURRENT) != 0); } } mDisplayModeDirector.setDesiredDisplayModeSpecsListener( Loading Loading @@ -1231,6 +1244,14 @@ public final class DisplayManagerService extends SystemService { Settings.Secure.getUriFor( MIRROR_BUILT_IN_DISPLAY), false, this, UserHandle.USER_ALL); } if (mFlags.isDefaultDisplayInTopologySwitchEnabled() && !mInjector.canInternalDisplayHostDesktops(mContext)) { mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor( Settings.Secure.INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY), false, this, UserHandle.USER_ALL); } } @Override Loading @@ -1253,6 +1274,15 @@ public final class DisplayManagerService extends SystemService { } return; } if (Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY).equals(uri)) { synchronized (mSyncRoot) { if (mFlags.isDefaultDisplayInTopologySwitchEnabled() && !mInjector.canInternalDisplayHostDesktops(mContext)) { handleIncludeDefaultDisplayInTopologySettingChangeLocked(); } } return; } } } Loading Loading @@ -1288,6 +1318,36 @@ public final class DisplayManagerService extends SystemService { return true; } private void handleIncludeDefaultDisplayInTopologySettingChangeLocked() { ContentResolver resolver = mContext.getContentResolver(); final boolean includeDefaultDisplayInTopology = Settings.Secure.getIntForUser(resolver, INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0, UserHandle.USER_CURRENT) != 0; if (mIncludeDefaultDisplayInTopology == includeDefaultDisplayInTopology) { return; } mIncludeDefaultDisplayInTopology = includeDefaultDisplayInTopology; // This switch is not available when the "Mirror built-in display" switch is enabled. if (shouldMirrorBuiltInDisplay()) { return; } if (mDisplayTopologyCoordinator != null) { if (mIncludeDefaultDisplayInTopology) { final DisplayInfo info = mLogicalDisplayMapper.getDisplayLocked( Display.DEFAULT_DISPLAY).getDisplayInfoLocked(); mDisplayTopologyCoordinator.onDisplayAdded(info); } else { // The default display can only be removed when there are multiple displays in the // topology to ensure the topology is not empty. if (mDisplayTopologyCoordinator.getTopology().hasMultipleDisplays()) { mDisplayTopologyCoordinator.onDisplayRemoved(Display.DEFAULT_DISPLAY); } } } } private void restoreResolutionFromBackup() { int savedMode = Settings.Secure.getIntForUser(mContext.getContentResolver(), Settings.Secure.SCREEN_RESOLUTION_MODE, Loading Loading @@ -2443,6 +2503,11 @@ public final class DisplayManagerService extends SystemService { } } boolean shouldIncludeDefaultDisplayInTopology() { return mInjector.canInternalDisplayHostDesktops(mContext) || mIncludeDefaultDisplayInTopology; } @SuppressLint("AndroidFrameworkRequiresPermission") private void handleLogicalDisplayAddedLocked(LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); Loading Loading @@ -3994,6 +4059,10 @@ public final class DisplayManagerService extends SystemService { blanker, logicalDisplay, brightnessTracker, brightnessSetting, onBrightnessChangeRunnable, hbmMetadata, bootCompleted, flags); } boolean canInternalDisplayHostDesktops(Context context) { return DesktopModeHelper.canInternalDisplayHostDesktops(context); } } @VisibleForTesting Loading services/core/java/com/android/server/display/DisplayTopologyCoordinator.java +56 −4 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.server.display; import android.annotation.Nullable; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayTopology; import android.hardware.display.DisplayTopologyGraph; import android.util.Pair; Loading @@ -26,6 +28,8 @@ import android.view.DisplayInfo; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; import com.android.server.display.feature.DisplayManagerFlags; import java.io.PrintWriter; import java.util.HashMap; Loading @@ -48,6 +52,8 @@ class DisplayTopologyCoordinator { return info.uniqueId; } private final Injector mInjector; // Persistent data store for display topologies. private final DisplayTopologyStore mTopologyStore; Loading @@ -65,6 +71,13 @@ class DisplayTopologyCoordinator { */ private final BooleanSupplier mIsExtendedDisplayAllowed; /** * Check if the default display should be included in the topology when there are other displays * present. If not, remove the default when another display is added, and add the default * display back to the topology when all other displays are removed. */ private final BooleanSupplier mShouldIncludeDefaultDisplayInTopology; /** * Callback used to send topology updates. * Should be invoked from the corresponding executor. Loading @@ -74,28 +87,35 @@ class DisplayTopologyCoordinator { private final Executor mTopologyChangeExecutor; private final DisplayManagerService.SyncRoot mSyncRoot; private final Runnable mTopologySavedCallback; private final DisplayManagerFlags mFlags; DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayAllowed, BooleanSupplier shouldIncludeDefaultDisplayInTopology, Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { this(new Injector(), isExtendedDisplayAllowed, onTopologyChangedCallback, topologyChangeExecutor, syncRoot, topologySavedCallback); Runnable topologySavedCallback, DisplayManagerFlags flags) { this(new Injector(), isExtendedDisplayAllowed, shouldIncludeDefaultDisplayInTopology, onTopologyChangedCallback, topologyChangeExecutor, syncRoot, topologySavedCallback, flags); } @VisibleForTesting DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayAllowed, BooleanSupplier shouldIncludeDefaultDisplayInTopology, Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { Runnable topologySavedCallback, DisplayManagerFlags flags) { mInjector = injector; mTopology = injector.getTopology(); mIsExtendedDisplayAllowed = isExtendedDisplayAllowed; mShouldIncludeDefaultDisplayInTopology = shouldIncludeDefaultDisplayInTopology; mOnTopologyChangedCallback = onTopologyChangedCallback; mTopologyChangeExecutor = topologyChangeExecutor; mSyncRoot = syncRoot; mTopologyStore = injector.createTopologyStore( mDisplayIdToUniqueIdMapping, mUniqueIdToDisplayIdMapping); mTopologySavedCallback = topologySavedCallback; mFlags = flags; } /** Loading @@ -114,6 +134,16 @@ class DisplayTopologyCoordinator { restoreTopologyLocked(); sendTopologyUpdateLocked(); } if (mFlags.isDefaultDisplayInTopologySwitchEnabled()) { // If the default display should not be included in the topology, then when a // non-default display is added, remove the default display from the topology. if (info.displayId != Display.DEFAULT_DISPLAY && !mShouldIncludeDefaultDisplayInTopology.getAsBoolean() && mTopology.hasMultipleDisplays()) { onDisplayRemoved(Display.DEFAULT_DISPLAY); } } } /** Loading Loading @@ -145,6 +175,16 @@ class DisplayTopologyCoordinator { sendTopologyUpdateLocked(); } } // If the default display should not be included in the topology, then when all non-default // displays are removed, add the default display back to the topology. if (mFlags.isDefaultDisplayInTopologySwitchEnabled()) { if (displayId != Display.DEFAULT_DISPLAY && !mShouldIncludeDefaultDisplayInTopology.getAsBoolean() && mTopology.isEmpty()) { onDisplayAdded(mInjector.getDisplayInfo(Display.DEFAULT_DISPLAY)); } } } /** Loading Loading @@ -225,6 +265,9 @@ class DisplayTopologyCoordinator { } private boolean isDisplayAllowedInTopology(DisplayInfo info, boolean shouldLog) { if (info == null) { return false; } if (info.type != Display.TYPE_INTERNAL && info.type != Display.TYPE_EXTERNAL && info.type != Display.TYPE_OVERLAY) { if (shouldLog) { Loading Loading @@ -275,6 +318,9 @@ class DisplayTopologyCoordinator { @VisibleForTesting static class Injector { private final DisplayManagerInternal mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); DisplayTopology getTopology() { return new DisplayTopology(); } Loading @@ -294,5 +340,11 @@ class DisplayTopologyCoordinator { } }); } @Nullable DisplayInfo getDisplayInfo(int displayId) { return mDisplayManagerInternal == null ? null : mDisplayManagerInternal.getDisplayInfo(displayId); } } } services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +139 −0 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUST import static android.hardware.display.DisplayManagerGlobal.INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED; import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_SYSTEM; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; import static android.provider.Settings.Secure.INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY; import static android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; Loading Loading @@ -64,6 +65,7 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; Loading Loading @@ -197,6 +199,7 @@ import org.mockito.MockitoAnnotations; import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; import java.lang.reflect.Field; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; Loading Loading @@ -380,6 +383,11 @@ public class DisplayManagerServiceTest { boolean getHdrOutputConversionSupport() { return true; } @Override boolean canInternalDisplayHostDesktops(Context context) { return false; } } private final DisplayManagerService.Injector mBasicInjector = new BasicInjector(); Loading @@ -406,6 +414,7 @@ public class DisplayManagerServiceTest { @Mock DisplayManagerInternal mMockDisplayManagerInternal; @Mock ActivityManagerInternal mMockActivityManagerInternal; @Mock DisplayAdapter mMockDisplayAdapter; @Mock DisplayTopologyCoordinator mMockDisplayTopologyCoordinator; @Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor; @Mock DisplayManagerFlags mMockFlags; Loading Loading @@ -4343,6 +4352,126 @@ public class DisplayManagerServiceTest { .getDefaultDozeBrightness(Display.DEFAULT_DISPLAY)); } @Test public void testIncludeDefaultDisplayInTopologySwitch_flagDisabled() { when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(false); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); setFieldValue(displayManager, "mDisplayTopologyCoordinator", mMockDisplayTopologyCoordinator); displayManager.systemReady(/* safeMode= */ false); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isFalse(); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isFalse(); verify(mMockDisplayTopologyCoordinator, never()).onDisplayAdded(any()); } @Test public void testIncludeDefaultDisplayInTopologySwitch_internalDisplayCanHostDesktops() { when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(true); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); DisplayManagerService displayManager = new DisplayManagerService( mContext, new BasicInjector() { @Override boolean canInternalDisplayHostDesktops(Context context) { return true; } }); displayManager.systemReady(/* safeMode= */ false); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isTrue(); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isTrue(); verify(mMockDisplayTopologyCoordinator, never()).onDisplayAdded(any()); } @Test public void testIncludeDefaultDisplayInTopologySwitch_addDefaultDisplayWhenEnableSwitch() { when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(true); Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); setFieldValue(displayManager, "mDisplayTopologyCoordinator", mMockDisplayTopologyCoordinator); displayManager.systemReady(/* safeMode= */ false); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isFalse(); createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL); DisplayInfo defaultDisplayInfo = displayManager.getLogicalDisplayMapper(). getDisplayLocked(Display.DEFAULT_DISPLAY).getDisplayInfoLocked(); clearInvocations(mMockDisplayTopologyCoordinator); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isTrue(); verify(mMockDisplayTopologyCoordinator).onDisplayAdded(defaultDisplayInfo); } @Test public void testIncludeDefaultDisplayInTopologySwitch_removeDefaultDisplayWhenDisableSwitch() { when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(true); Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); setFieldValue(displayManager, "mDisplayTopologyCoordinator", mMockDisplayTopologyCoordinator); displayManager.systemReady(/* safeMode= */ false); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isTrue(); DisplayTopology mockTopology = mock(DisplayTopology.class); doReturn(true).when(mockTopology).hasMultipleDisplays(); doReturn(mockTopology).when(mMockDisplayTopologyCoordinator).getTopology(); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isFalse(); verify(mMockDisplayTopologyCoordinator).onDisplayRemoved(Display.DEFAULT_DISPLAY); } @Test public void testIncludeDefaultDisplayInTopologySwitch_mirrorBuiltInDisplay() { when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(true); when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(true); Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); setFieldValue(displayManager, "mDisplayTopologyCoordinator", mMockDisplayTopologyCoordinator); displayManager.systemReady(/* safeMode= */ false); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); verify(mMockDisplayTopologyCoordinator, never()).onDisplayAdded(any()); } private void initDisplayPowerController(DisplayManagerInternal localService) { localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() { @Override Loading Loading @@ -4748,6 +4877,16 @@ public class DisplayManagerServiceTest { return topology; } private void setFieldValue(Object o, String fieldName, Object value) { try { final Field field = o.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(o, value); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException e) { throw new RuntimeException(e); } } private static class FakeDisplayManagerCallback extends IDisplayManagerCallback.Stub implements DisplayManagerInternal.DisplayGroupListener { int mDisplayId; Loading services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt +81 −2 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
core/java/android/hardware/display/DisplayTopology.java +14 −0 Original line number Diff line number Diff line Loading @@ -158,6 +158,20 @@ public final class DisplayTopology implements Parcelable { return mPrimaryDisplayId; } /** * @hide */ public boolean isEmpty() { return mRoot == null; } /** * @hide */ public boolean hasMultipleDisplays() { return mRoot != null && mRoot.mChildren != null && !mRoot.mChildren.isEmpty(); } /** * Add a display to the topology. * If this is the second display in the topology, it will be placed above the first display. Loading
services/core/java/com/android/server/display/DisplayManagerService.java +71 −2 Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.Process.FIRST_APPLICATION_UID; import static android.os.Process.ROOT_UID; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; import static android.provider.Settings.Secure.INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY; import static android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY; import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL; import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH; Loading Loading @@ -195,6 +196,7 @@ import com.android.server.display.utils.DebugUtils; import com.android.server.display.utils.SensorUtils; import com.android.server.input.InputManagerInternal; import com.android.server.utils.FoldSettingProvider; import com.android.server.wm.DesktopModeHelper; import com.android.server.wm.SurfaceAnimationThread; import com.android.server.wm.WindowManagerInternal; Loading Loading @@ -550,6 +552,10 @@ public final class DisplayManagerService extends SystemService { private boolean mMirrorBuiltInDisplay; // Whether default display should be included in the display topology. Note that this should // only be used for the devices in projected mode. private boolean mIncludeDefaultDisplayInTopology; private final BroadcastReceiver mIdleModeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Loading Loading @@ -691,8 +697,9 @@ public final class DisplayManagerService extends SystemService { deliverTopologyUpdate(update.first); }; mDisplayTopologyCoordinator = new DisplayTopologyCoordinator( this::isExtendedDisplayAllowed, topologyChangedCallback, new HandlerExecutor(mHandler), mSyncRoot, backupManager::dataChanged); this::isExtendedDisplayAllowed, this::shouldIncludeDefaultDisplayInTopology, topologyChangedCallback, new HandlerExecutor(mHandler), mSyncRoot, backupManager::dataChanged, mFlags); } else { mDisplayTopologyCoordinator = null; } Loading Loading @@ -875,6 +882,12 @@ public final class DisplayManagerService extends SystemService { if (mFlags.isDisplayContentModeManagementEnabled()) { updateMirrorBuiltInDisplaySettingLocked(/*shouldSendDisplayChangeEvent=*/ false); } if (mFlags.isDefaultDisplayInTopologySwitchEnabled()) { mIncludeDefaultDisplayInTopology = mInjector.canInternalDisplayHostDesktops( mContext) || (Settings.Secure.getIntForUser(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0, UserHandle.USER_CURRENT) != 0); } } mDisplayModeDirector.setDesiredDisplayModeSpecsListener( Loading Loading @@ -1231,6 +1244,14 @@ public final class DisplayManagerService extends SystemService { Settings.Secure.getUriFor( MIRROR_BUILT_IN_DISPLAY), false, this, UserHandle.USER_ALL); } if (mFlags.isDefaultDisplayInTopologySwitchEnabled() && !mInjector.canInternalDisplayHostDesktops(mContext)) { mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor( Settings.Secure.INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY), false, this, UserHandle.USER_ALL); } } @Override Loading @@ -1253,6 +1274,15 @@ public final class DisplayManagerService extends SystemService { } return; } if (Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY).equals(uri)) { synchronized (mSyncRoot) { if (mFlags.isDefaultDisplayInTopologySwitchEnabled() && !mInjector.canInternalDisplayHostDesktops(mContext)) { handleIncludeDefaultDisplayInTopologySettingChangeLocked(); } } return; } } } Loading Loading @@ -1288,6 +1318,36 @@ public final class DisplayManagerService extends SystemService { return true; } private void handleIncludeDefaultDisplayInTopologySettingChangeLocked() { ContentResolver resolver = mContext.getContentResolver(); final boolean includeDefaultDisplayInTopology = Settings.Secure.getIntForUser(resolver, INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0, UserHandle.USER_CURRENT) != 0; if (mIncludeDefaultDisplayInTopology == includeDefaultDisplayInTopology) { return; } mIncludeDefaultDisplayInTopology = includeDefaultDisplayInTopology; // This switch is not available when the "Mirror built-in display" switch is enabled. if (shouldMirrorBuiltInDisplay()) { return; } if (mDisplayTopologyCoordinator != null) { if (mIncludeDefaultDisplayInTopology) { final DisplayInfo info = mLogicalDisplayMapper.getDisplayLocked( Display.DEFAULT_DISPLAY).getDisplayInfoLocked(); mDisplayTopologyCoordinator.onDisplayAdded(info); } else { // The default display can only be removed when there are multiple displays in the // topology to ensure the topology is not empty. if (mDisplayTopologyCoordinator.getTopology().hasMultipleDisplays()) { mDisplayTopologyCoordinator.onDisplayRemoved(Display.DEFAULT_DISPLAY); } } } } private void restoreResolutionFromBackup() { int savedMode = Settings.Secure.getIntForUser(mContext.getContentResolver(), Settings.Secure.SCREEN_RESOLUTION_MODE, Loading Loading @@ -2443,6 +2503,11 @@ public final class DisplayManagerService extends SystemService { } } boolean shouldIncludeDefaultDisplayInTopology() { return mInjector.canInternalDisplayHostDesktops(mContext) || mIncludeDefaultDisplayInTopology; } @SuppressLint("AndroidFrameworkRequiresPermission") private void handleLogicalDisplayAddedLocked(LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); Loading Loading @@ -3994,6 +4059,10 @@ public final class DisplayManagerService extends SystemService { blanker, logicalDisplay, brightnessTracker, brightnessSetting, onBrightnessChangeRunnable, hbmMetadata, bootCompleted, flags); } boolean canInternalDisplayHostDesktops(Context context) { return DesktopModeHelper.canInternalDisplayHostDesktops(context); } } @VisibleForTesting Loading
services/core/java/com/android/server/display/DisplayTopologyCoordinator.java +56 −4 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.server.display; import android.annotation.Nullable; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayTopology; import android.hardware.display.DisplayTopologyGraph; import android.util.Pair; Loading @@ -26,6 +28,8 @@ import android.view.DisplayInfo; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; import com.android.server.display.feature.DisplayManagerFlags; import java.io.PrintWriter; import java.util.HashMap; Loading @@ -48,6 +52,8 @@ class DisplayTopologyCoordinator { return info.uniqueId; } private final Injector mInjector; // Persistent data store for display topologies. private final DisplayTopologyStore mTopologyStore; Loading @@ -65,6 +71,13 @@ class DisplayTopologyCoordinator { */ private final BooleanSupplier mIsExtendedDisplayAllowed; /** * Check if the default display should be included in the topology when there are other displays * present. If not, remove the default when another display is added, and add the default * display back to the topology when all other displays are removed. */ private final BooleanSupplier mShouldIncludeDefaultDisplayInTopology; /** * Callback used to send topology updates. * Should be invoked from the corresponding executor. Loading @@ -74,28 +87,35 @@ class DisplayTopologyCoordinator { private final Executor mTopologyChangeExecutor; private final DisplayManagerService.SyncRoot mSyncRoot; private final Runnable mTopologySavedCallback; private final DisplayManagerFlags mFlags; DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayAllowed, BooleanSupplier shouldIncludeDefaultDisplayInTopology, Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { this(new Injector(), isExtendedDisplayAllowed, onTopologyChangedCallback, topologyChangeExecutor, syncRoot, topologySavedCallback); Runnable topologySavedCallback, DisplayManagerFlags flags) { this(new Injector(), isExtendedDisplayAllowed, shouldIncludeDefaultDisplayInTopology, onTopologyChangedCallback, topologyChangeExecutor, syncRoot, topologySavedCallback, flags); } @VisibleForTesting DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayAllowed, BooleanSupplier shouldIncludeDefaultDisplayInTopology, Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { Runnable topologySavedCallback, DisplayManagerFlags flags) { mInjector = injector; mTopology = injector.getTopology(); mIsExtendedDisplayAllowed = isExtendedDisplayAllowed; mShouldIncludeDefaultDisplayInTopology = shouldIncludeDefaultDisplayInTopology; mOnTopologyChangedCallback = onTopologyChangedCallback; mTopologyChangeExecutor = topologyChangeExecutor; mSyncRoot = syncRoot; mTopologyStore = injector.createTopologyStore( mDisplayIdToUniqueIdMapping, mUniqueIdToDisplayIdMapping); mTopologySavedCallback = topologySavedCallback; mFlags = flags; } /** Loading @@ -114,6 +134,16 @@ class DisplayTopologyCoordinator { restoreTopologyLocked(); sendTopologyUpdateLocked(); } if (mFlags.isDefaultDisplayInTopologySwitchEnabled()) { // If the default display should not be included in the topology, then when a // non-default display is added, remove the default display from the topology. if (info.displayId != Display.DEFAULT_DISPLAY && !mShouldIncludeDefaultDisplayInTopology.getAsBoolean() && mTopology.hasMultipleDisplays()) { onDisplayRemoved(Display.DEFAULT_DISPLAY); } } } /** Loading Loading @@ -145,6 +175,16 @@ class DisplayTopologyCoordinator { sendTopologyUpdateLocked(); } } // If the default display should not be included in the topology, then when all non-default // displays are removed, add the default display back to the topology. if (mFlags.isDefaultDisplayInTopologySwitchEnabled()) { if (displayId != Display.DEFAULT_DISPLAY && !mShouldIncludeDefaultDisplayInTopology.getAsBoolean() && mTopology.isEmpty()) { onDisplayAdded(mInjector.getDisplayInfo(Display.DEFAULT_DISPLAY)); } } } /** Loading Loading @@ -225,6 +265,9 @@ class DisplayTopologyCoordinator { } private boolean isDisplayAllowedInTopology(DisplayInfo info, boolean shouldLog) { if (info == null) { return false; } if (info.type != Display.TYPE_INTERNAL && info.type != Display.TYPE_EXTERNAL && info.type != Display.TYPE_OVERLAY) { if (shouldLog) { Loading Loading @@ -275,6 +318,9 @@ class DisplayTopologyCoordinator { @VisibleForTesting static class Injector { private final DisplayManagerInternal mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); DisplayTopology getTopology() { return new DisplayTopology(); } Loading @@ -294,5 +340,11 @@ class DisplayTopologyCoordinator { } }); } @Nullable DisplayInfo getDisplayInfo(int displayId) { return mDisplayManagerInternal == null ? null : mDisplayManagerInternal.getDisplayInfo(displayId); } } }
services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +139 −0 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUST import static android.hardware.display.DisplayManagerGlobal.INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED; import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_SYSTEM; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; import static android.provider.Settings.Secure.INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY; import static android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; Loading Loading @@ -64,6 +65,7 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; Loading Loading @@ -197,6 +199,7 @@ import org.mockito.MockitoAnnotations; import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; import java.lang.reflect.Field; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; Loading Loading @@ -380,6 +383,11 @@ public class DisplayManagerServiceTest { boolean getHdrOutputConversionSupport() { return true; } @Override boolean canInternalDisplayHostDesktops(Context context) { return false; } } private final DisplayManagerService.Injector mBasicInjector = new BasicInjector(); Loading @@ -406,6 +414,7 @@ public class DisplayManagerServiceTest { @Mock DisplayManagerInternal mMockDisplayManagerInternal; @Mock ActivityManagerInternal mMockActivityManagerInternal; @Mock DisplayAdapter mMockDisplayAdapter; @Mock DisplayTopologyCoordinator mMockDisplayTopologyCoordinator; @Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor; @Mock DisplayManagerFlags mMockFlags; Loading Loading @@ -4343,6 +4352,126 @@ public class DisplayManagerServiceTest { .getDefaultDozeBrightness(Display.DEFAULT_DISPLAY)); } @Test public void testIncludeDefaultDisplayInTopologySwitch_flagDisabled() { when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(false); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); setFieldValue(displayManager, "mDisplayTopologyCoordinator", mMockDisplayTopologyCoordinator); displayManager.systemReady(/* safeMode= */ false); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isFalse(); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isFalse(); verify(mMockDisplayTopologyCoordinator, never()).onDisplayAdded(any()); } @Test public void testIncludeDefaultDisplayInTopologySwitch_internalDisplayCanHostDesktops() { when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(true); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); DisplayManagerService displayManager = new DisplayManagerService( mContext, new BasicInjector() { @Override boolean canInternalDisplayHostDesktops(Context context) { return true; } }); displayManager.systemReady(/* safeMode= */ false); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isTrue(); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isTrue(); verify(mMockDisplayTopologyCoordinator, never()).onDisplayAdded(any()); } @Test public void testIncludeDefaultDisplayInTopologySwitch_addDefaultDisplayWhenEnableSwitch() { when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(true); Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); setFieldValue(displayManager, "mDisplayTopologyCoordinator", mMockDisplayTopologyCoordinator); displayManager.systemReady(/* safeMode= */ false); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isFalse(); createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL); DisplayInfo defaultDisplayInfo = displayManager.getLogicalDisplayMapper(). getDisplayLocked(Display.DEFAULT_DISPLAY).getDisplayInfoLocked(); clearInvocations(mMockDisplayTopologyCoordinator); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isTrue(); verify(mMockDisplayTopologyCoordinator).onDisplayAdded(defaultDisplayInfo); } @Test public void testIncludeDefaultDisplayInTopologySwitch_removeDefaultDisplayWhenDisableSwitch() { when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(true); Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); setFieldValue(displayManager, "mDisplayTopologyCoordinator", mMockDisplayTopologyCoordinator); displayManager.systemReady(/* safeMode= */ false); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isTrue(); DisplayTopology mockTopology = mock(DisplayTopology.class); doReturn(true).when(mockTopology).hasMultipleDisplays(); doReturn(mockTopology).when(mMockDisplayTopologyCoordinator).getTopology(); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); assertThat(displayManager.shouldIncludeDefaultDisplayInTopology()).isFalse(); verify(mMockDisplayTopologyCoordinator).onDisplayRemoved(Display.DEFAULT_DISPLAY); } @Test public void testIncludeDefaultDisplayInTopologySwitch_mirrorBuiltInDisplay() { when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(true); when(mMockFlags.isDefaultDisplayInTopologySwitchEnabled()).thenReturn(true); Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 0); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); setFieldValue(displayManager, "mDisplayTopologyCoordinator", mMockDisplayTopologyCoordinator); displayManager.systemReady(/* safeMode= */ false); Settings.Secure.putInt(mContext.getContentResolver(), INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY, 1); final ContentObserver observer = displayManager.getSettingsObserver(); observer.onChange(false, Settings.Secure.getUriFor(INCLUDE_DEFAULT_DISPLAY_IN_TOPOLOGY)); verify(mMockDisplayTopologyCoordinator, never()).onDisplayAdded(any()); } private void initDisplayPowerController(DisplayManagerInternal localService) { localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() { @Override Loading Loading @@ -4748,6 +4877,16 @@ public class DisplayManagerServiceTest { return topology; } private void setFieldValue(Object o, String fieldName, Object value) { try { final Field field = o.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(o, value); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException e) { throw new RuntimeException(e); } } private static class FakeDisplayManagerCallback extends IDisplayManagerCallback.Stub implements DisplayManagerInternal.DisplayGroupListener { int mDisplayId; Loading
services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt +81 −2 File changed.Preview size limit exceeded, changes collapsed. Show changes