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

Commit 6ccbf099 authored by Piotr Wilczyński's avatar Piotr Wilczyński
Browse files

Topology listener API

Register and unregister topology listener APIs, similar to display listener APIs.

Extend the current Display Manager callback to also send topology updates.

A topology copy is sent in a topology update and it is sent without holding any locks.

Bug: 365075972
Change-Id: I78248f674b29f354d16b7d326be0bd8c19e167a8
Test: DisplayManagerGlobalTest, DisplayManagerServiceTest, DisplayTopologyCoordinatorTest, DisplayTopologyTest
Flag: com.android.server.display.feature.flags.display_topology
parent 5518e7f2
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
@@ -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;


@@ -1840,6 +1842,30 @@ public final class DisplayManager {
        mGlobal.setDisplayTopology(topology);
    }

    /**
     * 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.
     */
+123 −2
Original line number Diff line number Diff line
@@ -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 {
@@ -1509,6 +1605,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.
+19 −0
Original line number Diff line number Diff line
@@ -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
+3 −0
Original line number Diff line number Diff line
@@ -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);
}
+61 −37
Original line number Diff line number Diff line
@@ -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
Loading