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

Commit 9ec6eedc authored by Chris Li's avatar Chris Li
Browse files

Synchronize window config updates (3/n)

Reroute DisplayListener#onDisplayChanged from WindowManager. It will be
invoked when display-related window configuration is changed, such as
bounds and rotation.

This is to ensure #onDisplayChanged will be invoked after Configuration
update.

Also fix an existing DisplayManagerGlobalTest that is constantly failing
in postsubmit.

Also cleanup Message used in DisplayListenerDelegate since it is no
longer a Handler.

Bug: 260873529
Test: atest FrameworksCoreTests:DisplayManagerGlobalTest
Change-Id: I99f8579b539e3839e84e20ce4be3d50b324e7379
parent 4635cff9
Loading
Loading
Loading
Loading
+14 −71
Original line number Original line Diff line number Diff line
@@ -20,46 +20,31 @@ import static com.android.window.flags.Flags.syncWindowConfigUpdateFlag;


import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNull;


import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.NonNull;
import android.app.ActivityThread;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Process;
import android.os.Process;
import android.util.ArrayMap;


import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting;


import java.util.ArrayList;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;

/**
/**
 * Singleton controller to manage listeners to individual {@link ClientTransaction}.
 * Singleton controller to manage listeners to individual {@link ClientTransaction}.
 *
 *
 * TODO(b/260873529) make as TestApi to allow CTS.
 * @hide
 * @hide
 */
 */
public class ClientTransactionListenerController {
public class ClientTransactionListenerController {


    private static ClientTransactionListenerController sController;
    private static ClientTransactionListenerController sController;


    private final Object mLock = new Object();
    private final DisplayManagerGlobal mDisplayManager;

    /**
     * Mapping from client registered listener for display change to the corresponding
     * {@link Executor} to invoke the listener on.
     * @see #registerDisplayChangeListener(IntConsumer, Executor)
     */
    @GuardedBy("mLock")
    private final ArrayMap<IntConsumer, Executor> mDisplayChangeListeners = new ArrayMap<>();

    private final ArrayList<IntConsumer> mTmpDisplayChangeListeners = new ArrayList<>();


    /** Gets the singleton controller. */
    /** Gets the singleton controller. */
    @NonNull
    @NonNull
    public static ClientTransactionListenerController getInstance() {
    public static ClientTransactionListenerController getInstance() {
        synchronized (ClientTransactionListenerController.class) {
        synchronized (ClientTransactionListenerController.class) {
            if (sController == null) {
            if (sController == null) {
                sController = new ClientTransactionListenerController();
                sController = new ClientTransactionListenerController(
                        DisplayManagerGlobal.getInstance());
            }
            }
            return sController;
            return sController;
        }
        }
@@ -68,45 +53,13 @@ public class ClientTransactionListenerController {
    /** Creates a new instance for test only. */
    /** Creates a new instance for test only. */
    @VisibleForTesting
    @VisibleForTesting
    @NonNull
    @NonNull
    public static ClientTransactionListenerController createInstanceForTesting() {
    public static ClientTransactionListenerController createInstanceForTesting(
        return new ClientTransactionListenerController();
            @NonNull DisplayManagerGlobal displayManager) {
    }
        return new ClientTransactionListenerController(displayManager);

    private ClientTransactionListenerController() {}

    /**
     * Registers a new listener for display change. It will be invoked when receives a
     * {@link ClientTransaction} that is updating display-related window configuration, such as
     * bounds and rotation.
     *
     * WHen triggered, the listener will be invoked with the logical display id that was changed.
     *
     * @param listener the listener to invoke when receives a transaction with Display change.
     * @param executor the executor on which callback method will be invoked.
     */
    public void registerDisplayChangeListener(@NonNull IntConsumer listener,
            @NonNull @CallbackExecutor Executor executor) {
        if (!isSyncWindowConfigUpdateFlagEnabled()) {
            return;
        }
        requireNonNull(listener);
        requireNonNull(executor);
        synchronized (mLock) {
            mDisplayChangeListeners.put(listener, executor);
        }
    }
    }


    /**
    private ClientTransactionListenerController(@NonNull DisplayManagerGlobal displayManager) {
     * Unregisters the listener for display change that was previously registered through
        mDisplayManager = requireNonNull(displayManager);
     * {@link #registerDisplayChangeListener}.
     */
    public void unregisterDisplayChangeListener(@NonNull IntConsumer listener) {
        if (!isSyncWindowConfigUpdateFlagEnabled()) {
            return;
        }
        synchronized (mLock) {
            mDisplayChangeListeners.remove(listener);
        }
    }
    }


    /**
    /**
@@ -117,24 +70,14 @@ public class ClientTransactionListenerController {
        if (!isSyncWindowConfigUpdateFlagEnabled()) {
        if (!isSyncWindowConfigUpdateFlagEnabled()) {
            return;
            return;
        }
        }
        synchronized (mLock) {
        if (ActivityThread.isSystem()) {
            // Make a copy of the list to avoid listener removal during callback.
            // Not enable for system server.
            mTmpDisplayChangeListeners.addAll(mDisplayChangeListeners.keySet());
            return;
            final int num = mTmpDisplayChangeListeners.size();
            try {
                for (int i = 0; i < num; i++) {
                    final IntConsumer listener = mTmpDisplayChangeListeners.get(i);
                    final Executor executor = mDisplayChangeListeners.get(listener);
                    executor.execute(() -> listener.accept(displayId));
                }
            } finally {
                mTmpDisplayChangeListeners.clear();
            }
        }
        }
        mDisplayManager.handleDisplayChangeFromWindowManager(displayId);
    }
    }


    /** Whether {@link #syncWindowConfigUpdateFlag} feature flag is enabled. */
    /** Whether {@link #syncWindowConfigUpdateFlag} feature flag is enabled. */
    @VisibleForTesting
    public boolean isSyncWindowConfigUpdateFlagEnabled() {
    public boolean isSyncWindowConfigUpdateFlagEnabled() {
        // Can't read flag from isolated process.
        // Can't read flag from isolated process.
        return !Process.isIsolated() && syncWindowConfigUpdateFlag();
        return !Process.isIsolated() && syncWindowConfigUpdateFlag();
+39 −29
Original line number Original line Diff line number Diff line
@@ -42,7 +42,6 @@ import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.IBinder;
import android.os.Looper;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceManager;
import android.os.Trace;
import android.os.Trace;
@@ -412,6 +411,18 @@ public final class DisplayManagerGlobal {
        }
        }
    }
    }


    /**
     * 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.
     * @param displayId the logical display that was changed.
     */
    public void handleDisplayChangeFromWindowManager(int displayId) {
        // There can be racing condition between DMS and WMS callbacks, so force triggering the
        // listener to make sure the client can get the onDisplayChanged callback even if
        // DisplayInfo is not changed (Display read from both DisplayInfo and WindowConfiguration).
        handleDisplayEvent(displayId, EVENT_DISPLAY_CHANGED, true /* forceUpdate */);
    }

    private static Looper getLooperForHandler(@Nullable Handler handler) {
    private static Looper getLooperForHandler(@Nullable Handler handler) {
        Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
        Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
        if (looper == null) {
        if (looper == null) {
@@ -470,7 +481,7 @@ public final class DisplayManagerGlobal {
        }
        }
    }
    }


    private void handleDisplayEvent(int displayId, @DisplayEvent int event) {
    private void handleDisplayEvent(int displayId, @DisplayEvent int event, boolean forceUpdate) {
        final DisplayInfo info;
        final DisplayInfo info;
        synchronized (mLock) {
        synchronized (mLock) {
            if (USE_CACHE) {
            if (USE_CACHE) {
@@ -501,7 +512,7 @@ public final class DisplayManagerGlobal {
        // Accepting an Executor means the listener may be synchronously invoked, so we must
        // Accepting an Executor means the listener may be synchronously invoked, so we must
        // not be holding mLock when we do so
        // not be holding mLock when we do so
        for (DisplayListenerDelegate listener : mDisplayListeners) {
        for (DisplayListenerDelegate listener : mDisplayListeners) {
            listener.sendDisplayEvent(displayId, event, info);
            listener.sendDisplayEvent(displayId, event, info, forceUpdate);
        }
        }
    }
    }


@@ -1176,7 +1187,7 @@ public final class DisplayManagerGlobal {
                Log.d(TAG, "onDisplayEvent: displayId=" + displayId + ", event=" + eventToString(
                Log.d(TAG, "onDisplayEvent: displayId=" + displayId + ", event=" + eventToString(
                        event));
                        event));
            }
            }
            handleDisplayEvent(displayId, event);
            handleDisplayEvent(displayId, event, false /* forceUpdate */);
        }
        }
    }
    }


@@ -1197,87 +1208,86 @@ public final class DisplayManagerGlobal {
            mPackageName = packageName;
            mPackageName = packageName;
        }
        }


        public void sendDisplayEvent(int displayId, @DisplayEvent int event, DisplayInfo info) {
        void sendDisplayEvent(int displayId, @DisplayEvent int event, @Nullable DisplayInfo info,
                boolean forceUpdate) {
            if (extraLogging()) {
            if (extraLogging()) {
                Slog.i(TAG, "Sending Display Event: " + eventToString(event));
                Slog.i(TAG, "Sending Display Event: " + eventToString(event));
            }
            }
            long generationId = mGenerationId.get();
            long generationId = mGenerationId.get();
            Message msg = Message.obtain(null, event, displayId, 0, info);
            mExecutor.execute(() -> {
            mExecutor.execute(() -> {
                // If the generation id's don't match we were canceled but still need to recycle()
                // If the generation id's don't match we were canceled
                if (generationId == mGenerationId.get()) {
                if (generationId == mGenerationId.get()) {
                    handleMessage(msg);
                    handleDisplayEventInner(displayId, event, info, forceUpdate);
                }
                }
                msg.recycle();
            });
            });
        }
        }


        public void clearEvents() {
        void clearEvents() {
            mGenerationId.incrementAndGet();
            mGenerationId.incrementAndGet();
        }
        }


        public void setEventsMask(@EventsMask long newEventsMask) {
        void setEventsMask(@EventsMask long newEventsMask) {
            mEventsMask = newEventsMask;
            mEventsMask = newEventsMask;
        }
        }


        private void handleMessage(Message msg) {
        private void handleDisplayEventInner(int displayId, @DisplayEvent int event,
                @Nullable DisplayInfo info, boolean forceUpdate) {
            if (extraLogging()) {
            if (extraLogging()) {
                Slog.i(TAG, "DLD(" + eventToString(msg.what)
                Slog.i(TAG, "DLD(" + eventToString(event)
                        + ", display=" + msg.arg1
                        + ", display=" + displayId
                        + ", mEventsMask=" + Long.toBinaryString(mEventsMask)
                        + ", mEventsMask=" + Long.toBinaryString(mEventsMask)
                        + ", mPackageName=" + mPackageName
                        + ", mPackageName=" + mPackageName
                        + ", msg.obj=" + msg.obj
                        + ", displayInfo=" + info
                        + ", listener=" + mListener.getClass() + ")");
                        + ", listener=" + mListener.getClass() + ")");
            }
            }
            if (DEBUG) {
            if (DEBUG) {
                Trace.beginSection(
                Trace.beginSection(
                        TextUtils.trimToSize(
                        TextUtils.trimToSize(
                                "DLD(" + eventToString(msg.what)
                                "DLD(" + eventToString(event)
                                + ", display=" + msg.arg1
                                + ", display=" + displayId
                                + ", listener=" + mListener.getClass() + ")", 127));
                                + ", listener=" + mListener.getClass() + ")", 127));
            }
            }
            switch (msg.what) {
            switch (event) {
                case EVENT_DISPLAY_ADDED:
                case EVENT_DISPLAY_ADDED:
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0) {
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0) {
                        mListener.onDisplayAdded(msg.arg1);
                        mListener.onDisplayAdded(displayId);
                    }
                    }
                    break;
                    break;
                case EVENT_DISPLAY_CHANGED:
                case EVENT_DISPLAY_CHANGED:
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) {
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) {
                        DisplayInfo newInfo = (DisplayInfo) msg.obj;
                        if (info != null && (forceUpdate || !info.equals(mDisplayInfo))) {
                        if (newInfo != null && !newInfo.equals(mDisplayInfo)) {
                            if (extraLogging()) {
                            if (extraLogging()) {
                                Slog.i(TAG, "Sending onDisplayChanged: Display Changed. Info: "
                                Slog.i(TAG, "Sending onDisplayChanged: Display Changed. Info: "
                                        + newInfo);
                                        + info);
                            }
                            }
                            mDisplayInfo.copyFrom(newInfo);
                            mDisplayInfo.copyFrom(info);
                            mListener.onDisplayChanged(msg.arg1);
                            mListener.onDisplayChanged(displayId);
                        }
                        }
                    }
                    }
                    break;
                    break;
                case EVENT_DISPLAY_BRIGHTNESS_CHANGED:
                case EVENT_DISPLAY_BRIGHTNESS_CHANGED:
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0) {
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0) {
                        mListener.onDisplayChanged(msg.arg1);
                        mListener.onDisplayChanged(displayId);
                    }
                    }
                    break;
                    break;
                case EVENT_DISPLAY_REMOVED:
                case EVENT_DISPLAY_REMOVED:
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0) {
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0) {
                        mListener.onDisplayRemoved(msg.arg1);
                        mListener.onDisplayRemoved(displayId);
                    }
                    }
                    break;
                    break;
                case EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED:
                case EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED:
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0) {
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0) {
                        mListener.onDisplayChanged(msg.arg1);
                        mListener.onDisplayChanged(displayId);
                    }
                    }
                    break;
                    break;
                case EVENT_DISPLAY_CONNECTED:
                case EVENT_DISPLAY_CONNECTED:
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
                        mListener.onDisplayConnected(msg.arg1);
                        mListener.onDisplayConnected(displayId);
                    }
                    }
                    break;
                    break;
                case EVENT_DISPLAY_DISCONNECTED:
                case EVENT_DISPLAY_DISCONNECTED:
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
                        mListener.onDisplayDisconnected(msg.arg1);
                        mListener.onDisplayDisconnected(displayId);
                    }
                    }
                    break;
                    break;
            }
            }
+26 −17
Original line number Original line Diff line number Diff line
@@ -16,14 +16,19 @@


package android.app.servertransaction;
package android.app.servertransaction;


import static org.mockito.ArgumentMatchers.anyInt;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.mockito.Mockito.clearInvocations;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verify;


import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.IDisplayManager;
import android.os.Handler;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.Presubmit;
import android.view.DisplayInfo;


import androidx.test.filters.SmallTest;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.runner.AndroidJUnit4;
@@ -34,8 +39,6 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoAnnotations;


import java.util.function.IntConsumer;

/**
/**
 * Tests for {@link ClientTransactionListenerController}.
 * Tests for {@link ClientTransactionListenerController}.
 *
 *
@@ -47,30 +50,36 @@ import java.util.function.IntConsumer;
@Presubmit
@Presubmit
public class ClientTransactionListenerControllerTest {
public class ClientTransactionListenerControllerTest {
    @Mock
    @Mock
    private IntConsumer mDisplayChangeListener;
    private IDisplayManager mIDisplayManager;
    @Mock
    private DisplayManager.DisplayListener mListener;


    private DisplayManagerGlobal mDisplayManager;
    private Handler mHandler;
    private ClientTransactionListenerController mController;
    private ClientTransactionListenerController mController;


    @Before
    @Before
    public void setup() {
    public void setup() {
        MockitoAnnotations.initMocks(this);
        MockitoAnnotations.initMocks(this);
        mController = spy(ClientTransactionListenerController.createInstanceForTesting());
        mDisplayManager = new DisplayManagerGlobal(mIDisplayManager);
        mHandler = getInstrumentation().getContext().getMainThreadHandler();
        mController = spy(ClientTransactionListenerController.createInstanceForTesting(
                mDisplayManager));
        doReturn(true).when(mController).isSyncWindowConfigUpdateFlagEnabled();
        doReturn(true).when(mController).isSyncWindowConfigUpdateFlagEnabled();
    }
    }


    @Test
    @Test
    public void testRegisterDisplayChangeListener() {
    public void testOnDisplayChanged() throws RemoteException {
        mController.registerDisplayChangeListener(mDisplayChangeListener, Runnable::run);
        // Mock IDisplayManager to return a display info to trigger display change.
        final DisplayInfo newDisplayInfo = new DisplayInfo();
        doReturn(newDisplayInfo).when(mIDisplayManager).getDisplayInfo(123);


        mController.onDisplayChanged(123);
        mDisplayManager.registerDisplayListener(mListener, mHandler,
                DisplayManager.EVENT_FLAG_DISPLAY_CHANGED, null /* packageName */);


        verify(mDisplayChangeListener).accept(123);
        mController.onDisplayChanged(123);

        mHandler.runWithScissors(() -> { }, 0);
        clearInvocations(mDisplayChangeListener);
        mController.unregisterDisplayChangeListener(mDisplayChangeListener);

        mController.onDisplayChanged(321);


        verify(mDisplayChangeListener, never()).accept(anyInt());
        verify(mListener).onDisplayChanged(123);
    }
    }
}
}
+59 −1
Original line number Original line Diff line number Diff line
@@ -16,12 +16,19 @@


package android.hardware.display;
package android.hardware.display;


import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;


import android.content.Context;
import android.content.Context;
import android.os.Handler;
import android.os.Handler;
import android.os.RemoteException;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.view.DisplayInfo;


import androidx.test.InstrumentationRegistry;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.filters.SmallTest;
@@ -37,6 +44,13 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoAnnotations;


/**
 * Tests for {@link DisplayManagerGlobal}.
 *
 * Build/Install/Run:
 *  atest FrameworksCoreTests:DisplayManagerGlobalTest
 */
@Presubmit
@SmallTest
@SmallTest
@RunWith(AndroidJUnit4.class)
@RunWith(AndroidJUnit4.class)
public class DisplayManagerGlobalTest {
public class DisplayManagerGlobalTest {
@@ -51,6 +65,9 @@ public class DisplayManagerGlobalTest {
    @Mock
    @Mock
    private DisplayManager.DisplayListener mListener;
    private DisplayManager.DisplayListener mListener;


    @Mock
    private DisplayManager.DisplayListener mListener2;

    @Captor
    @Captor
    private ArgumentCaptor<IDisplayManagerCallback> mCallbackCaptor;
    private ArgumentCaptor<IDisplayManagerCallback> mCallbackCaptor;


@@ -82,7 +99,11 @@ public class DisplayManagerGlobalTest {
        Mockito.verifyNoMoreInteractions(mListener);
        Mockito.verifyNoMoreInteractions(mListener);


        Mockito.reset(mListener);
        Mockito.reset(mListener);
        callback.onDisplayEvent(1, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED);
        // 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();
        waitForHandler();
        Mockito.verify(mListener).onDisplayChanged(eq(displayId));
        Mockito.verify(mListener).onDisplayChanged(eq(displayId));
        Mockito.verifyNoMoreInteractions(mListener);
        Mockito.verifyNoMoreInteractions(mListener);
@@ -161,7 +182,44 @@ public class DisplayManagerGlobalTest {
        mDisplayManagerGlobal.unregisterDisplayListener(mListener);
        mDisplayManagerGlobal.unregisterDisplayListener(mListener);
        inOrder.verify(mDisplayManager)
        inOrder.verify(mDisplayManager)
                .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(0L));
                .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(0L));
    }

    @Test
    public void testHandleDisplayChangeFromWindowManager() throws RemoteException {
        // Mock IDisplayManager to return a display info to trigger display change.
        final DisplayInfo newDisplayInfo = new DisplayInfo();
        doReturn(newDisplayInfo).when(mDisplayManager).getDisplayInfo(123);
        doReturn(newDisplayInfo).when(mDisplayManager).getDisplayInfo(321);

        // Nothing happens when there is no listener.
        mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(123);

        // One listener listens on add/remove, and the other one listens on change.
        mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler,
                DisplayManager.EVENT_FLAG_DISPLAY_ADDED
                        | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED, null /* packageName */);
        mDisplayManagerGlobal.registerDisplayListener(mListener2, mHandler,
                DisplayManager.EVENT_FLAG_DISPLAY_CHANGED, null /* packageName */);

        mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(321);
        waitForHandler();

        verify(mListener, never()).onDisplayChanged(anyInt());
        verify(mListener2).onDisplayChanged(321);

        // Trigger the callback again even if the display info is not changed.
        clearInvocations(mListener2);
        mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(321);
        waitForHandler();

        verify(mListener2).onDisplayChanged(321);

        // No callback for non-existing display (no display info returned from IDisplayManager).
        clearInvocations(mListener2);
        mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(456);
        waitForHandler();


        verify(mListener2, never()).onDisplayChanged(anyInt());
    }
    }


    private void waitForHandler() {
    private void waitForHandler() {