diff options
| author | 2024-11-22 16:10:35 +0000 | |
|---|---|---|
| committer | 2024-11-22 16:10:35 +0000 | |
| commit | 2d8946ad99326382fcb0944dc9c34081be016104 (patch) | |
| tree | 7c01fee83b2ea73d442baea9aecff7877e095d9b | |
| parent | 640f25eb172853cc23bc8f12498cc0c7b3d5c1de (diff) | |
| parent | 2ddac774862d4a77355463681478e9cd5bdce947 (diff) | |
Merge "Topology listener API" into main
10 files changed, 478 insertions, 91 deletions
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 25327a9b1d52..7054c37cbc3b 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -24,6 +24,7 @@ import static android.view.Display.INVALID_DISPLAY; import static com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS; import android.Manifest; +import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; @@ -70,6 +71,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.Predicate; @@ -1841,6 +1843,30 @@ public final class DisplayManager { } /** + * Register a listener to receive display topology updates. + * @param executor The executor specifying the thread on which the callbacks will be invoked + * @param listener The listener + * + * @hide + */ + @RequiresPermission(MANAGE_DISPLAYS) + public void registerTopologyListener(@NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<DisplayTopology> listener) { + mGlobal.registerTopologyListener(executor, listener, ActivityThread.currentPackageName()); + } + + /** + * Unregister a display topology listener. + * @param listener The listener to unregister + * + * @hide + */ + @RequiresPermission(MANAGE_DISPLAYS) + public void unregisterTopologyListener(@NonNull Consumer<DisplayTopology> listener) { + mGlobal.unregisterTopologyListener(listener); + } + + /** * Listens for changes in available display devices. */ public interface DisplayListener { diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 1e66beea42a6..1eec65619d4e 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -23,6 +23,7 @@ import static android.Manifest.permission.MANAGE_DISPLAYS; import static android.view.Display.HdrCapabilities.HdrType; import android.Manifest; +import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; @@ -73,6 +74,7 @@ import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; /** * Manager communication with the display manager service on behalf of @@ -126,7 +128,7 @@ public final class DisplayManagerGlobal { public static final int EVENT_DISPLAY_REFRESH_RATE_CHANGED = 8; public static final int EVENT_DISPLAY_STATE_CHANGED = 9; - @LongDef(prefix = {"INTERNAL_EVENT_DISPLAY"}, flag = true, value = { + @LongDef(prefix = {"INTERNAL_EVENT_FLAG_"}, flag = true, value = { INTERNAL_EVENT_FLAG_DISPLAY_ADDED, INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, @@ -134,7 +136,8 @@ public final class DisplayManagerGlobal { INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED, INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, - INTERNAL_EVENT_FLAG_DISPLAY_STATE + INTERNAL_EVENT_FLAG_DISPLAY_STATE, + INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED, }) @Retention(RetentionPolicy.SOURCE) public @interface InternalEventFlag {} @@ -147,6 +150,7 @@ public final class DisplayManagerGlobal { public static final long INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED = 1L << 5; public static final long INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE = 1L << 6; public static final long INTERNAL_EVENT_FLAG_DISPLAY_STATE = 1L << 7; + public static final long INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED = 1L << 8; @UnsupportedAppUsage private static DisplayManagerGlobal sInstance; @@ -164,6 +168,9 @@ public final class DisplayManagerGlobal { private final CopyOnWriteArrayList<DisplayListenerDelegate> mDisplayListeners = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList<DisplayTopologyListenerDelegate> mTopologyListeners = + new CopyOnWriteArrayList<>(); + private final SparseArray<DisplayInfo> mDisplayInfoCache = new SparseArray<>(); private final ColorSpace mWideColorSpace; private final OverlayProperties mOverlayProperties; @@ -457,6 +464,18 @@ public final class DisplayManagerGlobal { } } + private void maybeLogAllTopologyListeners() { + if (!extraLogging()) { + return; + } + Slog.i(TAG, "Currently registered display topology listeners:"); + int i = 0; + for (DisplayTopologyListenerDelegate d : mTopologyListeners) { + Slog.i(TAG, i + ": " + d); + i++; + } + } + /** * Called when there is a display-related window configuration change. Reroutes the event from * WindowManager to make sure the {@link Display} fields are up-to-date in the last callback. @@ -502,9 +521,22 @@ public final class DisplayManagerGlobal { | INTERNAL_EVENT_FLAG_DISPLAY_CHANGED | INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; } + if (!mTopologyListeners.isEmpty()) { + mask |= INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED; + } return mask; } + private DisplayTopologyListenerDelegate findTopologyListenerLocked( + @NonNull Consumer<DisplayTopology> listener) { + for (DisplayTopologyListenerDelegate delegate : mTopologyListeners) { + if (delegate.mListener == listener) { + return delegate; + } + } + return null; + } + private void registerCallbackIfNeededLocked() { if (mCallback == null) { mCallback = new DisplayManagerCallback(); @@ -1316,6 +1348,9 @@ public final class DisplayManagerGlobal { */ @RequiresPermission(MANAGE_DISPLAYS) public void setDisplayTopology(DisplayTopology topology) { + if (topology == null) { + throw new IllegalArgumentException("Topology must not be null"); + } try { mDm.setDisplayTopology(topology); } catch (RemoteException ex) { @@ -1323,6 +1358,57 @@ public final class DisplayManagerGlobal { } } + /** + * @see DisplayManager#registerTopologyListener + */ + @RequiresPermission(MANAGE_DISPLAYS) + public void registerTopologyListener(@NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<DisplayTopology> listener, String packageName) { + if (!Flags.displayTopology()) { + return; + } + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + if (extraLogging()) { + Slog.i(TAG, "Registering display topology listener: packageName=" + packageName); + } + synchronized (mLock) { + DisplayTopologyListenerDelegate delegate = findTopologyListenerLocked(listener); + if (delegate == null) { + mTopologyListeners.add(new DisplayTopologyListenerDelegate(listener, executor, + packageName)); + registerCallbackIfNeededLocked(); + updateCallbackIfNeededLocked(); + } + maybeLogAllTopologyListeners(); + } + } + + /** + * @see DisplayManager#unregisterTopologyListener + */ + @RequiresPermission(MANAGE_DISPLAYS) + public void unregisterTopologyListener(@NonNull Consumer<DisplayTopology> listener) { + if (!Flags.displayTopology()) { + return; + } + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + if (extraLogging()) { + Slog.i(TAG, "Unregistering display topology listener: " + listener); + } + synchronized (mLock) { + DisplayTopologyListenerDelegate delegate = findTopologyListenerLocked(listener); + if (delegate != null) { + mTopologyListeners.remove(delegate); + updateCallbackIfNeededLocked(); + } + } + maybeLogAllTopologyListeners(); + } + private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub { @Override public void onDisplayEvent(int displayId, @DisplayEvent int event) { @@ -1332,6 +1418,16 @@ public final class DisplayManagerGlobal { } handleDisplayEvent(displayId, event, false /* forceUpdate */); } + + @Override + public void onTopologyChanged(DisplayTopology topology) { + if (DEBUG) { + Log.d(TAG, "onTopologyChanged: " + topology); + } + for (DisplayTopologyListenerDelegate listener : mTopologyListeners) { + listener.onTopologyChanged(topology); + } + } } private static final class DisplayListenerDelegate { @@ -1516,6 +1612,31 @@ public final class DisplayManagerGlobal { } } + private static final class DisplayTopologyListenerDelegate { + private final Consumer<DisplayTopology> mListener; + private final Executor mExecutor; + private final String mPackageName; + + DisplayTopologyListenerDelegate(@NonNull Consumer<DisplayTopology> listener, + @NonNull @CallbackExecutor Executor executor, String packageName) { + mExecutor = executor; + mListener = listener; + mPackageName = packageName; + } + + @Override + public String toString() { + return "DisplayTopologyListener {packageName=" + mPackageName + "}"; + } + + void onTopologyChanged(DisplayTopology topology) { + if (extraLogging()) { + Slog.i(TAG, "Sending topology update: " + topology); + } + mExecutor.execute(() -> mListener.accept(topology)); + } + } + /** * The API portion of the key that identifies the unique PropertyInvalidatedCache token which * changes every time we update the system's display configuration. diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java index f00c3a53ad0c..0e53d873e43c 100644 --- a/core/java/android/hardware/display/DisplayTopology.java +++ b/core/java/android/hardware/display/DisplayTopology.java @@ -283,6 +283,14 @@ public final class DisplayTopology implements Parcelable { normalize(); } + /** + * @return A deep copy of the topology that will not be modified by the system. + */ + public DisplayTopology copy() { + TreeNode rootCopy = mRoot == null ? null : mRoot.copy(); + return new DisplayTopology(rootCopy, mPrimaryDisplayId); + } + @Override public int describeContents() { return 0; @@ -694,6 +702,17 @@ public final class DisplayTopology implements Parcelable { return Collections.unmodifiableList(mChildren); } + /** + * @return A deep copy of the node that will not be modified by the system. + */ + public TreeNode copy() { + TreeNode copy = new TreeNode(mDisplayId, mWidth, mHeight, mPosition, mOffset); + for (TreeNode child : mChildren) { + copy.mChildren.add(child.copy()); + } + return copy; + } + @Override public String toString() { return "Display {id=" + mDisplayId + ", width=" + mWidth + ", height=" + mHeight diff --git a/core/java/android/hardware/display/IDisplayManagerCallback.aidl b/core/java/android/hardware/display/IDisplayManagerCallback.aidl index c50e3fb26156..d05a1b8400b0 100644 --- a/core/java/android/hardware/display/IDisplayManagerCallback.aidl +++ b/core/java/android/hardware/display/IDisplayManagerCallback.aidl @@ -16,7 +16,10 @@ package android.hardware.display; +import android.hardware.display.DisplayTopology; + /** @hide */ interface IDisplayManagerCallback { oneway void onDisplayEvent(int displayId, int event); + oneway void onTopologyChanged(in DisplayTopology topology); } diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index 6a5224d4524b..7a5b3064b3a3 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -55,6 +55,9 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + /** * Tests for {@link DisplayManagerGlobal}. * @@ -79,10 +82,13 @@ public class DisplayManagerGlobalTest { private IDisplayManager mDisplayManager; @Mock - private DisplayManager.DisplayListener mListener; + private DisplayManager.DisplayListener mDisplayListener; + + @Mock + private DisplayManager.DisplayListener mDisplayListener2; @Mock - private DisplayManager.DisplayListener mListener2; + private Consumer<DisplayTopology> mTopologyListener; @Captor private ArgumentCaptor<IDisplayManagerCallback> mCallbackCaptor; @@ -90,6 +96,7 @@ public class DisplayManagerGlobalTest { private Context mContext; private DisplayManagerGlobal mDisplayManagerGlobal; private Handler mHandler; + private Executor mExecutor; @Before public void setUp() throws RemoteException { @@ -97,13 +104,14 @@ public class DisplayManagerGlobalTest { Mockito.when(mDisplayManager.getPreferredWideGamutColorSpaceId()).thenReturn(0); mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); mHandler = mContext.getMainThreadHandler(); + mExecutor = mContext.getMainExecutor(); mDisplayManagerGlobal = new DisplayManagerGlobal(mDisplayManager); } @Test public void testDisplayListenerIsCalled_WhenDisplayEventOccurs() throws RemoteException { - mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, ALL_DISPLAY_EVENTS, - null); + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + ALL_DISPLAY_EVENTS, /* packageName= */ null); Mockito.verify(mDisplayManager) .registerCallbackWithEventMask(mCallbackCaptor.capture(), anyLong()); IDisplayManagerCallback callback = mCallbackCaptor.getValue(); @@ -111,31 +119,31 @@ public class DisplayManagerGlobalTest { int displayId = 1; callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_ADDED); waitForHandler(); - Mockito.verify(mListener).onDisplayAdded(eq(displayId)); - Mockito.verifyNoMoreInteractions(mListener); + Mockito.verify(mDisplayListener).onDisplayAdded(eq(displayId)); + Mockito.verifyNoMoreInteractions(mDisplayListener); - Mockito.reset(mListener); + Mockito.reset(mDisplayListener); // Mock IDisplayManager to return a different display info to trigger display change. final DisplayInfo newDisplayInfo = new DisplayInfo(); newDisplayInfo.rotation++; doReturn(newDisplayInfo).when(mDisplayManager).getDisplayInfo(displayId); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); waitForHandler(); - Mockito.verify(mListener).onDisplayChanged(eq(displayId)); - Mockito.verifyNoMoreInteractions(mListener); + Mockito.verify(mDisplayListener).onDisplayChanged(eq(displayId)); + Mockito.verifyNoMoreInteractions(mDisplayListener); - Mockito.reset(mListener); + Mockito.reset(mDisplayListener); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); waitForHandler(); - Mockito.verify(mListener).onDisplayRemoved(eq(displayId)); - Mockito.verifyNoMoreInteractions(mListener); + Mockito.verify(mDisplayListener).onDisplayRemoved(eq(displayId)); + Mockito.verifyNoMoreInteractions(mDisplayListener); } @Test @RequiresFlagsEnabled(Flags.FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) public void testDisplayListenerIsCalled_WhenDisplayPropertyChangeEventOccurs() throws RemoteException { - mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | INTERNAL_EVENT_FLAG_DISPLAY_STATE, null); @@ -145,50 +153,50 @@ public class DisplayManagerGlobalTest { int displayId = 1; - Mockito.reset(mListener); + Mockito.reset(mDisplayListener); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_REFRESH_RATE_CHANGED); waitForHandler(); - Mockito.verify(mListener).onDisplayChanged(eq(displayId)); - Mockito.verifyNoMoreInteractions(mListener); + Mockito.verify(mDisplayListener).onDisplayChanged(eq(displayId)); + Mockito.verifyNoMoreInteractions(mDisplayListener); - Mockito.reset(mListener); + Mockito.reset(mDisplayListener); callback.onDisplayEvent(displayId, EVENT_DISPLAY_STATE_CHANGED); waitForHandler(); - Mockito.verify(mListener).onDisplayChanged(eq(displayId)); - Mockito.verifyNoMoreInteractions(mListener); + Mockito.verify(mDisplayListener).onDisplayChanged(eq(displayId)); + Mockito.verifyNoMoreInteractions(mDisplayListener); } @Test public void testDisplayListenerIsNotCalled_WhenClientIsNotSubscribed() throws RemoteException { // First we subscribe to all events in order to test that the subsequent calls to // registerDisplayListener will update the event mask. - mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, ALL_DISPLAY_EVENTS, - null); + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + ALL_DISPLAY_EVENTS, /* packageName= */ null); Mockito.verify(mDisplayManager) .registerCallbackWithEventMask(mCallbackCaptor.capture(), anyLong()); IDisplayManagerCallback callback = mCallbackCaptor.getValue(); int displayId = 1; - mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, ALL_DISPLAY_EVENTS & ~DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED, null); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_ADDED); waitForHandler(); - Mockito.verifyZeroInteractions(mListener); + Mockito.verifyZeroInteractions(mDisplayListener); - mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, ALL_DISPLAY_EVENTS & ~DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, null); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); waitForHandler(); - Mockito.verifyZeroInteractions(mListener); + Mockito.verifyZeroInteractions(mDisplayListener); - mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, ALL_DISPLAY_EVENTS & ~DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, null); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); waitForHandler(); - Mockito.verifyZeroInteractions(mListener); + Mockito.verifyZeroInteractions(mDisplayListener); } @Test @@ -207,7 +215,7 @@ public class DisplayManagerGlobalTest { @Test public void testDisplayManagerGlobalRegistersWithDisplayManager_WhenThereAreListeners() throws RemoteException { - mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED, null); InOrder inOrder = Mockito.inOrder(mDisplayManager); @@ -228,7 +236,7 @@ public class DisplayManagerGlobalTest { .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED)); - mDisplayManagerGlobal.unregisterDisplayListener(mListener); + mDisplayManagerGlobal.unregisterDisplayListener(mDisplayListener); inOrder.verify(mDisplayManager) .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(0L)); } @@ -244,33 +252,49 @@ public class DisplayManagerGlobalTest { mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(123); // One listener listens on add/remove, and the other one listens on change. - mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, null /* packageName */); - mDisplayManagerGlobal.registerDisplayListener(mListener2, mHandler, + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener2, mHandler, DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, null /* packageName */); mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(321); waitForHandler(); - verify(mListener, never()).onDisplayChanged(anyInt()); - verify(mListener2).onDisplayChanged(321); + verify(mDisplayListener, never()).onDisplayChanged(anyInt()); + verify(mDisplayListener2).onDisplayChanged(321); // Trigger the callback again even if the display info is not changed. - clearInvocations(mListener2); + clearInvocations(mDisplayListener2); mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(321); waitForHandler(); - verify(mListener2).onDisplayChanged(321); + verify(mDisplayListener2).onDisplayChanged(321); // No callback for non-existing display (no display info returned from IDisplayManager). - clearInvocations(mListener2); + clearInvocations(mDisplayListener2); mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(456); waitForHandler(); - verify(mListener2, never()).onDisplayChanged(anyInt()); + verify(mDisplayListener2, never()).onDisplayChanged(anyInt()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DISPLAY_TOPOLOGY) + public void testTopologyListenerIsCalled_WhenTopologyUpdateOccurs() throws RemoteException { + mDisplayManagerGlobal.registerTopologyListener(mExecutor, mTopologyListener, + /* packageName= */ null); + Mockito.verify(mDisplayManager).registerCallbackWithEventMask(mCallbackCaptor.capture(), + eq(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED)); + IDisplayManagerCallback callback = mCallbackCaptor.getValue(); + + DisplayTopology topology = new DisplayTopology(); + callback.onTopologyChanged(topology); + waitForHandler(); + Mockito.verify(mTopologyListener).accept(topology); + Mockito.verifyNoMoreInteractions(mTopologyListener); } @Test diff --git a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt index 8969b2b72e77..f584ab971d04 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt +++ b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt @@ -513,7 +513,7 @@ class DisplayTopologyTest { val nodes = rearrangeRects( RectF(0f, 0f, 150f, 100f), RectF(-150f, 0f, 0f, 100f), - RectF(0f,-100f, 150f, 0f), + RectF(0f, -100f, 150f, 0f), RectF(150f, 0f, 300f, 100f), RectF(0f, 100f, 150f, 200f), ) @@ -584,15 +584,15 @@ class DisplayTopologyTest { @Test fun rearrange_useLargerEdge() { val nodes = rearrangeRects( - //444111 - //444111 - //444111 - // 000222 - // 000222 - // 000222 - // 333 - // 333 - // 333 + // 444111 + // 444111 + // 444111 + // 000222 + // 000222 + // 000222 + // 333 + // 333 + // 333 RectF(20f, 30f, 50f, 60f), RectF(30f, 0f, 60f, 30f), RectF(50f, 30f, 80f, 60f), @@ -617,24 +617,25 @@ class DisplayTopologyTest { @Test fun rearrange_closeGaps() { val nodes = rearrangeRects( - //000 - //000 111 - //000 111 - // 111 + // 000 + // 000 111 + // 000 111 + // 111 // - // 222 - // 222 - // 222 + // 222 + // 222 + // 222 RectF(0f, 0f, 30f, 30f), RectF(40f, 10f, 70f, 40f), - RectF(80.5f, 50f, 110f, 80f), // left+=0.5 to cause a preference for TOP/BOTTOM attach + RectF(80.5f, 50f, 110f, 80f), // left+=0.5 to cause a preference for + // TOP/BOTTOM attach ) assertPositioning( nodes, // In the case of corner adjacency, we prefer a left/right attachment. Pair(POSITION_RIGHT, 10f), - Pair(POSITION_BOTTOM, 40.5f), // TODO: fix implementation to remove this gap + Pair(POSITION_BOTTOM, 40.5f), // TODO: fix implementation to remove this gap ) assertThat(nodes[0].children).containsExactly(nodes[1]) @@ -642,11 +643,65 @@ class DisplayTopologyTest { assertThat(nodes[2].children).isEmpty() } + @Test + fun copy() { + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, + /* height= */ 600f, /* position= */ 0, /* offset= */ 0f) + + val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f) + display1.addChild(display2) + + val primaryDisplayId = 3 + val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f) + display1.addChild(display3) + + val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, + /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) + display2.addChild(display4) + + topology = DisplayTopology(display1, primaryDisplayId) + val copy = topology.copy() + + assertThat(copy.primaryDisplayId).isEqualTo(primaryDisplayId) + + val actualDisplay1 = copy.root!! + assertThat(actualDisplay1.displayId).isEqualTo(1) + assertThat(actualDisplay1.width).isEqualTo(200f) + assertThat(actualDisplay1.height).isEqualTo(600f) + assertThat(actualDisplay1.children).hasSize(2) + + val actualDisplay2 = actualDisplay1.children[0] + assertThat(actualDisplay2.displayId).isEqualTo(2) + assertThat(actualDisplay2.width).isEqualTo(600f) + assertThat(actualDisplay2.height).isEqualTo(200f) + assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay2.offset).isEqualTo(0f) + assertThat(actualDisplay2.children).hasSize(1) + + val actualDisplay3 = actualDisplay1.children[1] + assertThat(actualDisplay3.displayId).isEqualTo(3) + assertThat(actualDisplay3.width).isEqualTo(600f) + assertThat(actualDisplay3.height).isEqualTo(200f) + assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay3.offset).isEqualTo(400f) + assertThat(actualDisplay3.children).isEmpty() + + val actualDisplay4 = actualDisplay2.children[0] + assertThat(actualDisplay4.displayId).isEqualTo(4) + assertThat(actualDisplay4.width).isEqualTo(200f) + assertThat(actualDisplay4.height).isEqualTo(600f) + assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay4.offset).isEqualTo(0f) + assertThat(actualDisplay4.children).isEmpty() + } + /** * Runs the rearrange algorithm and returns the resulting tree as a list of nodes, with the * root at index 0. The number of nodes is inferred from the number of positions passed. */ - private fun rearrangeRects(vararg pos : RectF) : List<DisplayTopology.TreeNode> { + private fun rearrangeRects(vararg pos: RectF): List<DisplayTopology.TreeNode> { // Generates a linear sequence of nodes in order in the List from root to leaf, // left-to-right. IDs are ascending from 0 to count - 1. @@ -668,7 +723,7 @@ class DisplayTopologyTest { } private fun assertPositioning( - nodes : List<DisplayTopology.TreeNode>, vararg positions : Pair<Int, Float>) { + nodes: List<DisplayTopology.TreeNode>, vararg positions: Pair<Int, Float>) { assertThat(nodes.drop(1).map { Pair(it.position, it.offset )}) .containsExactly(*positions) .inOrder() diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 3871f2a57f76..c74943c95a6a 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -668,7 +668,8 @@ public final class DisplayManagerService extends SystemService { mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector()); if (mFlags.isDisplayTopologyEnabled()) { mDisplayTopologyCoordinator = - new DisplayTopologyCoordinator(this::isExtendedDisplayEnabled); + new DisplayTopologyCoordinator(this::isExtendedDisplayEnabled, + this::deliverTopologyUpdate, new HandlerExecutor(mHandler), mSyncRoot); } else { mDisplayTopologyCoordinator = null; } @@ -3502,6 +3503,28 @@ public final class DisplayManagerService extends SystemService { callbackRecord.notifyDisplayEventAsync(displayId, event); } + private void deliverTopologyUpdate(DisplayTopology topology) { + if (DEBUG) { + Slog.d(TAG, "Delivering topology update"); + } + if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { + Trace.instant(Trace.TRACE_TAG_POWER, "deliverTopologyUpdate"); + } + + // Grab the lock and copy the callbacks. + List<CallbackRecord> callbacks = new ArrayList<>(); + synchronized (mSyncRoot) { + for (int i = 0; i < mCallbacks.size(); i++) { + callbacks.add(mCallbacks.valueAt(i)); + } + } + + // After releasing the lock, send the notifications out. + for (CallbackRecord callback : callbacks) { + callback.notifyTopologyUpdateAsync(topology); + } + } + private boolean extraLogging(String packageName) { return mExtraDisplayEventLogging && mExtraDisplayLoggingPackageName.equals(packageName); } @@ -4137,7 +4160,7 @@ public final class DisplayManagerService extends SystemService { * cached or frozen. */ public boolean notifyDisplayEventAsync(int displayId, @DisplayEvent int event) { - if (!shouldSendEvent(event)) { + if (!shouldSendDisplayEvent(event)) { if (extraLogging(mPackageName)) { Slog.i(TAG, "Not sending displayEvent: " + event + " due to mask:" @@ -4191,7 +4214,7 @@ public final class DisplayManagerService extends SystemService { /** * Return true if the client is interested in this event. */ - private boolean shouldSendEvent(@DisplayEvent int event) { + private boolean shouldSendDisplayEvent(@DisplayEvent int event) { final long mask = mInternalEventFlagsMask.get(); switch (event) { case DisplayManagerGlobal.EVENT_DISPLAY_ADDED: @@ -4252,6 +4275,45 @@ public final class DisplayManagerService extends SystemService { mPendingEvents.add(new Event(displayId, event)); } + /** + * @return {@code false} if RemoteException happens; otherwise {@code true} for + * success. + */ + boolean notifyTopologyUpdateAsync(DisplayTopology topology) { + if ((mInternalEventFlagsMask.get() + & DisplayManagerGlobal.INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED) == 0) { + if (extraLogging(mPackageName)) { + Slog.i(TAG, "Not sending topology update: " + topology + " due to mask: " + + mInternalEventFlagsMask); + } + if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { + Trace.instant(Trace.TRACE_TAG_POWER, + "notifyTopologyUpdateAsync#notSendingUpdate=" + topology + + ",mInternalEventFlagsMask=" + mInternalEventFlagsMask); + } + // The client is not interested in this event, so do nothing. + return true; + } + return transmitTopologyUpdate(topology); + } + + /** + * Transmit a single display topology update. The client is presumed ready. Return true on + * success and false if the client died. + */ + private boolean transmitTopologyUpdate(DisplayTopology topology) { + // The client is ready to receive the event. + try { + mCallback.onTopologyChanged(topology); + return true; + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify process " + + mPid + " that display topology changed, assuming it died.", ex); + binderDied(); + return false; + } + } + // Send all pending events. This can safely be called if the process is not ready, but it // would be unusual to do so. The method returns true on success. // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java index 47226861545f..55b292aefec4 100644 --- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java +++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java @@ -25,7 +25,9 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; +import java.util.concurrent.Executor; import java.util.function.BooleanSupplier; +import java.util.function.Consumer; /** * Manages the relative placement (topology) of extended displays. Responsible for updating and @@ -33,7 +35,7 @@ import java.util.function.BooleanSupplier; */ class DisplayTopologyCoordinator { - @GuardedBy("mLock") + @GuardedBy("mSyncRoot") private DisplayTopology mTopology; /** @@ -41,16 +43,31 @@ class DisplayTopologyCoordinator { */ private final BooleanSupplier mIsExtendedDisplayEnabled; - private final Object mLock = new Object(); - - DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayEnabled) { - this(new Injector(), isExtendedDisplayEnabled); + /** + * Callback used to send topology updates. + * Should be invoked from the corresponding executor. + * A copy of the topology should be sent that will not be modified by the system. + */ + private final Consumer<DisplayTopology> mOnTopologyChangedCallback; + private final Executor mTopologyChangeExecutor; + private final DisplayManagerService.SyncRoot mSyncRoot; + + DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayEnabled, + Consumer<DisplayTopology> onTopologyChangedCallback, + Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot) { + this(new Injector(), isExtendedDisplayEnabled, onTopologyChangedCallback, + topologyChangeExecutor, syncRoot); } @VisibleForTesting - DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayEnabled) { + DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayEnabled, + Consumer<DisplayTopology> onTopologyChangedCallback, + Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot) { mTopology = injector.getTopology(); mIsExtendedDisplayEnabled = isExtendedDisplayEnabled; + mOnTopologyChangedCallback = onTopologyChangedCallback; + mTopologyChangeExecutor = topologyChangeExecutor; + mSyncRoot = syncRoot; } /** @@ -61,8 +78,9 @@ class DisplayTopologyCoordinator { if (!isDisplayAllowedInTopology(info)) { return; } - synchronized (mLock) { + synchronized (mSyncRoot) { mTopology.addDisplay(info.displayId, getWidth(info), getHeight(info)); + sendTopologyUpdateLocked(); } } @@ -71,8 +89,9 @@ class DisplayTopologyCoordinator { * @param displayId The logical display ID */ void onDisplayRemoved(int displayId) { - synchronized (mLock) { + synchronized (mSyncRoot) { mTopology.removeDisplay(displayId); + sendTopologyUpdateLocked(); } } @@ -80,14 +99,15 @@ class DisplayTopologyCoordinator { * @return A deep copy of the topology. */ DisplayTopology getTopology() { - synchronized (mLock) { + synchronized (mSyncRoot) { return mTopology; } } void setTopology(DisplayTopology topology) { - synchronized (mLock) { + synchronized (mSyncRoot) { mTopology = topology; + sendTopologyUpdateLocked(); } } @@ -96,7 +116,7 @@ class DisplayTopologyCoordinator { * @param pw The stream to dump information to. */ void dump(PrintWriter pw) { - synchronized (mLock) { + synchronized (mSyncRoot) { mTopology.dump(pw); } } @@ -124,6 +144,12 @@ class DisplayTopologyCoordinator { && info.displayGroupId == Display.DEFAULT_DISPLAY_GROUP; } + @GuardedBy("mSyncRoot") + private void sendTopologyUpdateLocked() { + DisplayTopology copy = mTopology.copy(); + mTopologyChangeExecutor.execute(() -> mOnTopologyChangedCallback.accept(copy)); + } + @VisibleForTesting static class Injector { DisplayTopology getTopology() { 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 47e96d378149..3843404132c1 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -234,6 +234,7 @@ public class DisplayManagerServiceTest { 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"; + private static final String TOPOLOGY_CHANGED_EVENT = "TOPOLOGY_CHANGED_EVENT"; @Rule(order = 0) public TestRule compatChangeRule = new PlatformCompatChangeRule(); @@ -3699,8 +3700,7 @@ public class DisplayManagerServiceTest { DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 1); manageDisplaysPermission(/* granted= */ true); when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); - DisplayManagerService displayManager = - new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService displayManagerBinderService = displayManager.new BinderService(); @@ -3718,8 +3718,7 @@ public class DisplayManagerServiceTest { public void testGetDisplayTopology_NullIfFlagDisabled() { manageDisplaysPermission(/* granted= */ true); when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(false); - DisplayManagerService displayManager = - new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService displayManagerBinderService = displayManager.new BinderService(); @@ -3733,8 +3732,7 @@ public class DisplayManagerServiceTest { @Test public void testGetDisplayTopology_withoutPermission_shouldThrowException() { when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); - DisplayManagerService displayManager = - new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService displayManagerBinderService = displayManager.new BinderService(); @@ -3748,8 +3746,7 @@ public class DisplayManagerServiceTest { public void testSetDisplayTopology() { manageDisplaysPermission(/* granted= */ true); when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); - DisplayManagerService displayManager = - new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService displayManagerBinderService = displayManager.new BinderService(); @@ -3762,8 +3759,7 @@ public class DisplayManagerServiceTest { @Test public void testSetDisplayTopology_withoutPermission_shouldThrowException() { when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); - DisplayManagerService displayManager = - new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService displayManagerBinderService = displayManager.new BinderService(); @@ -3774,6 +3770,49 @@ public class DisplayManagerServiceTest { () -> displayManagerBinderService.setDisplayTopology(new DisplayTopology())); } + @Test + public void testShouldNotifyTopologyChanged() { + manageDisplaysPermission(/* granted= */ true); + when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + Handler handler = displayManager.getDisplayHandler(); + waitForIdleHandler(handler); + + FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); + displayManagerBinderService.registerCallbackWithEventMask(callback, + DisplayManagerGlobal.INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED); + waitForIdleHandler(handler); + + displayManagerBinderService.setDisplayTopology(new DisplayTopology()); + waitForIdleHandler(handler); + + assertThat(callback.receivedEvents()).containsExactly(TOPOLOGY_CHANGED_EVENT); + } + + @Test + public void testShouldNotNotifyTopologyChanged_WhenClientIsNotSubscribed() { + manageDisplaysPermission(/* granted= */ true); + when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + Handler handler = displayManager.getDisplayHandler(); + waitForIdleHandler(handler); + + // Only subscribe to display events, not topology events + FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); + displayManagerBinderService.registerCallbackWithEventMask(callback, + STANDARD_DISPLAY_EVENTS); + waitForIdleHandler(handler); + + displayManagerBinderService.setDisplayTopology(new DisplayTopology()); + waitForIdleHandler(handler); + + assertThat(callback.receivedEvents()).isEmpty(); + } + private void initDisplayPowerController(DisplayManagerInternal localService) { localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() { @Override @@ -4225,6 +4264,12 @@ public class DisplayManagerServiceTest { eventSeen(DISPLAY_GROUP_EVENT_CHANGED); } + @Override + public void onTopologyChanged(DisplayTopology topology) { + mReceivedEvents.add(TOPOLOGY_CHANGED_EVENT); + eventSeen(TOPOLOGY_CHANGED_EVENT); + } + public void clear() { mReceivedEvents.clear(); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt index a2d2a81b20b4..e4b461f307d3 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt @@ -24,18 +24,21 @@ import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.util.function.BooleanSupplier class DisplayTopologyCoordinatorTest { private lateinit var coordinator: DisplayTopologyCoordinator private val displayInfo = DisplayInfo() + private val topologyChangeExecutor = Runnable::run private val mockTopology = mock<DisplayTopology>() - private val mockIsExtendedDisplayEnabled = mock<BooleanSupplier>() + private val mockTopologyCopy = mock<DisplayTopology>() + private val mockIsExtendedDisplayEnabled = mock<() -> Boolean>() + private val mockTopologyChangedCallback = mock<(DisplayTopology) -> Unit>() @Before fun setUp() { @@ -47,13 +50,14 @@ class DisplayTopologyCoordinatorTest { val injector = object : DisplayTopologyCoordinator.Injector() { override fun getTopology() = mockTopology } - coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayEnabled) + whenever(mockIsExtendedDisplayEnabled()).thenReturn(true) + whenever(mockTopology.copy()).thenReturn(mockTopologyCopy) + coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayEnabled, + mockTopologyChangedCallback, topologyChangeExecutor, DisplayManagerService.SyncRoot()) } @Test fun addDisplay() { - whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(true) - coordinator.onDisplayAdded(displayInfo) val widthDp = displayInfo.logicalWidth * (DisplayMetrics.DENSITY_DEFAULT.toFloat() @@ -61,24 +65,26 @@ class DisplayTopologyCoordinatorTest { val heightDp = displayInfo.logicalHeight * (DisplayMetrics.DENSITY_DEFAULT.toFloat() / displayInfo.logicalDensityDpi) verify(mockTopology).addDisplay(displayInfo.displayId, widthDp, heightDp) + verify(mockTopologyChangedCallback).invoke(mockTopologyCopy) } @Test fun addDisplay_extendedDisplaysDisabled() { - whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(false) + whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) coordinator.onDisplayAdded(displayInfo) verify(mockTopology, never()).addDisplay(anyInt(), anyFloat(), anyFloat()) + verify(mockTopologyChangedCallback, never()).invoke(any()) } @Test fun addDisplay_notInDefaultDisplayGroup() { - whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(true) displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1 coordinator.onDisplayAdded(displayInfo) verify(mockTopology, never()).addDisplay(anyInt(), anyFloat(), anyFloat()) + verify(mockTopologyChangedCallback, never()).invoke(any()) } }
\ No newline at end of file |