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

Commit 6792eda4 authored by Piotr Wilczyński's avatar Piotr Wilczyński
Browse files

Send topology updates to frozen processes

Use the same approach as display events - create an activity, cache/freeze it, then send topology updates and wake up the activity.

Extract common code to the TestBase class.

Don't send a null topology graph to Input Manager.

Bug: 379093437
Bug: 399182507
Flag: com.android.server.display.feature.flags.display_topology
Test: DisplayManagerServiceTest, DisplayEventDeliveryTest, TopologyUpdateDeliveryTest
Change-Id: Ib931de4d712ac22843fcc0b9d7565fb92f94791b
parent 76ccff7d
Loading
Loading
Loading
Loading
+75 −34
Original line number Diff line number Diff line
@@ -684,8 +684,9 @@ public final class DisplayManagerService extends SystemService {
            final var backupManager = new BackupManager(mContext);
            Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> topologyChangedCallback =
                    update -> {
                        if (mInputManagerInternal != null) {
                            mInputManagerInternal.setDisplayTopology(update.second);
                        DisplayTopologyGraph graph = update.second;
                        if (mInputManagerInternal != null && graph != null) {
                            mInputManagerInternal.setDisplayTopology(graph);
                        }
                        deliverTopologyUpdate(update.first);
                    };
@@ -3620,7 +3621,7 @@ public final class DisplayManagerService extends SystemService {

    private void deliverTopologyUpdate(DisplayTopology topology) {
        if (DEBUG) {
            Slog.d(TAG, "Delivering topology update");
            Slog.d(TAG, "Delivering topology update: " + topology);
        }
        if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) {
            Trace.instant(Trace.TRACE_TAG_POWER, "deliverTopologyUpdate");
@@ -4179,13 +4180,18 @@ public final class DisplayManagerService extends SystemService {

        public boolean mWifiDisplayScanRequested;

        // A single pending event.
        // A single pending display event.
        private record Event(int displayId, @DisplayEvent int event) { };

        // The list of pending events.  This is null until there is a pending event to be saved.
        // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
        // The list of pending display events. This is null until there is a pending event to be
        // saved. This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
        @GuardedBy("mCallback")
        private ArrayList<Event> mPendingEvents;
        @Nullable
        private ArrayList<Event> mPendingDisplayEvents;

        @GuardedBy("mCallback")
        @Nullable
        private DisplayTopology mPendingTopology;

        // Process states: a process is ready to receive events if it is neither cached nor
        // frozen.
@@ -4255,7 +4261,10 @@ public final class DisplayManagerService extends SystemService {
         */
        @GuardedBy("mCallback")
        private boolean hasPendingAndIsReadyLocked() {
            return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty() && mAlive;
            boolean pendingDisplayEvents = mPendingDisplayEvents != null
                    && !mPendingDisplayEvents.isEmpty();
            boolean pendingTopology = mPendingTopology != null;
            return isReadyLocked() && (pendingDisplayEvents || pendingTopology) && mAlive;
        }

        /**
@@ -4336,7 +4345,8 @@ public final class DisplayManagerService extends SystemService {
                    // occurs as the client is transitioning to ready but pending events have not
                    // been dispatched.  The new event must be added to the pending list to
                    // preserve event ordering.
                    if (!isReadyLocked() || (mPendingEvents != null && !mPendingEvents.isEmpty())) {
                    if (!isReadyLocked() || (mPendingDisplayEvents != null
                            && !mPendingDisplayEvents.isEmpty())) {
                        // The client is interested in the event but is not ready to receive it.
                        // Put the event on the pending list.
                        addDisplayEvent(displayId, event);
@@ -4420,13 +4430,13 @@ public final class DisplayManagerService extends SystemService {
        // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
        @GuardedBy("mCallback")
        private void addDisplayEvent(int displayId, int event) {
            if (mPendingEvents == null) {
                mPendingEvents = new ArrayList<>();
            if (mPendingDisplayEvents == null) {
                mPendingDisplayEvents = new ArrayList<>();
            }
            if (!mPendingEvents.isEmpty()) {
            if (!mPendingDisplayEvents.isEmpty()) {
                // Ignore redundant events. Further optimization is possible by merging adjacent
                // events.
                Event last = mPendingEvents.get(mPendingEvents.size() - 1);
                Event last = mPendingDisplayEvents.get(mPendingDisplayEvents.size() - 1);
                if (last.displayId == displayId && last.event == event) {
                    if (DEBUG) {
                        Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event
@@ -4435,12 +4445,13 @@ public final class DisplayManagerService extends SystemService {
                    return;
                }
            }
            mPendingEvents.add(new Event(displayId, event));
            mPendingDisplayEvents.add(new Event(displayId, event));
        }

        /**
         * @return {@code false} if RemoteException happens; otherwise {@code true} for
         * success.
         * success. This returns true even if the update was deferred because the remote client is
         * cached or frozen.
         */
        boolean notifyTopologyUpdateAsync(DisplayTopology topology) {
            if ((mInternalEventFlagsMask.get()
@@ -4457,6 +4468,18 @@ public final class DisplayManagerService extends SystemService {
                // The client is not interested in this event, so do nothing.
                return true;
            }

            if (deferDisplayEventsWhenFrozen()) {
                synchronized (mCallback) {
                    // Save the new update if the client frozen or cached (not ready).
                    if (!isReadyLocked()) {
                        // The client is interested in the update but is not ready to receive it.
                        mPendingTopology = topology;
                        return true;
                    }
                }
            }

            return transmitTopologyUpdate(topology);
        }

@@ -4481,21 +4504,29 @@ public final class DisplayManagerService extends SystemService {
        // would be unusual to do so.  The method returns true on success.
        // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
        public boolean dispatchPending() {
            Event[] pending;
            Event[] pendingDisplayEvents = null;
            DisplayTopology pendingTopology;
            synchronized (mCallback) {
                if (mPendingEvents == null || mPendingEvents.isEmpty() || !mAlive) {
                if (!mAlive) {
                    return true;
                }
                if (!isReadyLocked()) {
                    return false;
                }
                pending = new Event[mPendingEvents.size()];
                pending = mPendingEvents.toArray(pending);
                mPendingEvents.clear();

                if (mPendingDisplayEvents != null && !mPendingDisplayEvents.isEmpty()) {
                    pendingDisplayEvents = new Event[mPendingDisplayEvents.size()];
                    pendingDisplayEvents = mPendingDisplayEvents.toArray(pendingDisplayEvents);
                    mPendingDisplayEvents.clear();
                }

                pendingTopology = mPendingTopology;
                mPendingTopology = null;
            }
            try {
                for (int i = 0; i < pending.length; i++) {
                    Event displayEvent = pending[i];
                if (pendingDisplayEvents != null) {
                    for (int i = 0; i < pendingDisplayEvents.length; i++) {
                        Event displayEvent = pendingDisplayEvents[i];
                        if (DEBUG) {
                            Slog.d(TAG, "Send pending display event #" + i + " "
                                    + displayEvent.displayId + "/"
@@ -4508,10 +4539,19 @@ public final class DisplayManagerService extends SystemService {

                        transmitDisplayEvent(displayEvent.displayId, displayEvent.event);
                    }
                }

                if (pendingTopology != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "Send pending topology: " + pendingTopology
                                + " to " + mUid + "/" + mPid);
                    }
                    mCallback.onTopologyChanged(pendingTopology);
                }

                return true;
            } catch (RemoteException ex) {
                Slog.w(TAG, "Failed to notify process "
                        + mPid + " that display topology changed, assuming it died.", ex);
                Slog.w(TAG, "Failed to notify process " + mPid + ", assuming it died.", ex);
                binderDied();
                return false;

@@ -4523,11 +4563,12 @@ public final class DisplayManagerService extends SystemService {
            if (deferDisplayEventsWhenFrozen()) {
                final String fmt =
                        "mPid=%d mUid=%d mWifiDisplayScanRequested=%s"
                        + " cached=%s frozen=%s pending=%d";
                        + " cached=%s frozen=%s pendingDisplayEvents=%d pendingTopology=%b";
                synchronized (mCallback) {
                    return formatSimple(fmt,
                            mPid, mUid, mWifiDisplayScanRequested, mCached, mFrozen,
                            (mPendingEvents == null) ? 0 : mPendingEvents.size());
                            (mPendingDisplayEvents == null) ? 0 : mPendingDisplayEvents.size(),
                            mPendingTopology != null);
                }
            } else {
                final String fmt =
+1 −0
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ android_test {

    data: [
        ":DisplayManagerTestApp",
        ":TopologyTestApp",
    ],

    certificate: "platform",
+1 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@
    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
    <uses-permission android:name="android.permission.MANAGE_USB" />
    <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />

    <!-- Permissions needed for DisplayTransformManagerTest -->
    <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
+1 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@
        <option name="cleanup-apks" value="true" />
        <option name="install-arg" value="-t" />
        <option name="test-file-name" value="DisplayManagerTestApp.apk" />
        <option name="test-file-name" value="TopologyTestApp.apk" />
    </target_preparer>

    <option name="test-tag" value="DisplayServiceTests" />
+32 −212
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.server.display;

import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
import static android.util.DisplayMetrics.DENSITY_HIGH;
@@ -27,19 +26,11 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import android.app.ActivityManager;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.os.BinderProxy;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.platform.test.annotations.AppModeSdkSandbox;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -48,10 +39,7 @@ import android.util.SparseArray;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.compatibility.common.util.SystemUtil;
import com.android.compatibility.common.util.TestUtils;
import com.android.server.am.Flags;

import org.junit.After;
@@ -63,9 +51,7 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

@@ -73,8 +59,7 @@ import java.util.concurrent.TimeUnit;
 * Tests that applications can receive display events correctly.
 */
@RunWith(Parameterized.class)
@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
public class DisplayEventDeliveryTest {
public class DisplayEventDeliveryTest extends EventDeliveryTestBase {
    private static final String TAG = "DisplayEventDeliveryTest";

    @Rule
@@ -85,37 +70,17 @@ public class DisplayEventDeliveryTest {
    private static final int WIDTH = 720;
    private static final int HEIGHT = 480;

    private static final int MESSAGE_LAUNCHED = 1;
    private static final int MESSAGE_CALLBACK = 2;

    private static final int DISPLAY_ADDED = 1;
    private static final int DISPLAY_CHANGED = 2;
    private static final int DISPLAY_REMOVED = 3;

    private static final long DISPLAY_EVENT_TIMEOUT_MSEC = 100;
    private static final long TEST_FAILURE_TIMEOUT_MSEC = 10000;

    private static final String TEST_PACKAGE =
            "com.android.servicestests.apps.displaymanagertestapp";
    private static final String TEST_ACTIVITY = TEST_PACKAGE + ".DisplayEventActivity";
    private static final String TEST_DISPLAYS = "DISPLAYS";
    private static final String TEST_MESSENGER = "MESSENGER";

    private final Object mLock = new Object();

    private Instrumentation mInstrumentation;
    private Context mContext;
    private DisplayManager mDisplayManager;
    private ActivityManager mActivityManager;
    private ActivityManager.OnUidImportanceListener mUidImportanceListener;
    private CountDownLatch mLatchActivityLaunch;
    private CountDownLatch mLatchActivityCached;
    private HandlerThread mHandlerThread;
    private Handler mHandler;
    private Messenger mMessenger;
    private int mPid;
    private int mUid;

    /**
     * Array of DisplayBundle. The test handler uses it to check if certain display events have
     * been sent to DisplayEventActivity.
@@ -167,7 +132,7 @@ public class DisplayEventDeliveryTest {
         */
        public void assertNoDisplayEvents() {
            try {
                assertNull(mExpectations.poll(DISPLAY_EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS));
                assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
@@ -239,37 +204,17 @@ public class DisplayEventDeliveryTest {
    }

    @Before
    public void setUp() throws Exception {
        mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mContext = mInstrumentation.getContext();
        mDisplayManager = mContext.getSystemService(DisplayManager.class);
        mLatchActivityLaunch = new CountDownLatch(1);
        mLatchActivityCached = new CountDownLatch(1);
        mActivityManager = mContext.getSystemService(ActivityManager.class);
        mUidImportanceListener = (uid, importance) -> {
            if (uid == mUid && importance == IMPORTANCE_CACHED) {
                Log.d(TAG, "Listener " + uid + " becomes " + importance);
                mLatchActivityCached.countDown();
            }
        };
        SystemUtil.runWithShellPermissionIdentity(() ->
                mActivityManager.addOnUidImportanceListener(mUidImportanceListener,
                        IMPORTANCE_CACHED));
    public void setUp() {
        super.setUp();
        // The lock is not functionally necessary but eliminates lint error messages.
        synchronized (mLock) {
            mDisplayBundles = new SparseArray<>();
        }
        mHandlerThread = new HandlerThread("handler");
        mHandlerThread.start();
        mHandler = new TestHandler(mHandlerThread.getLooper());
        mMessenger = new Messenger(mHandler);
        mPid = 0;
    }

    @After
    public void tearDown() throws Exception {
        mActivityManager.removeOnUidImportanceListener(mUidImportanceListener);
        mHandlerThread.quitSafely();
        super.tearDown();
        synchronized (mLock) {
            for (int i = 0; i < mDisplayBundles.size(); i++) {
                DisplayBundle bundle = mDisplayBundles.valueAt(i);
@@ -278,55 +223,45 @@ public class DisplayEventDeliveryTest {
            }
            mDisplayBundles.clear();
        }
        SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + TEST_PACKAGE);
    }

    /**
     * Return a display bundle at the stated index.  The bundle is retrieved under lock.
     */
    private DisplayBundle displayBundleAt(int i) {
        synchronized (mLock) {
            return mDisplayBundles.valueAt(i);
    @Override
    protected String getTag() {
        return TAG;
    }

    @Override
    protected Handler getHandler(Looper looper) {
        return new TestHandler(looper);
    }

    /**
     * Return true if the freezer is enabled on this platform and if freezer notifications are
     * supported.  It is not enough to test that the freezer notification feature is enabled
     * because some devices do not have the necessary kernel support.
     */
    private boolean isAppFreezerEnabled() {
        try {
            return mActivityManager.getService().isAppFreezerEnabled()
                    && android.os.Flags.binderFrozenStateChangeCallback()
                    && BinderProxy.isFrozenStateChangeCallbackSupported();
        } catch (Exception e) {
            Log.e(TAG, "isAppFreezerEnabled() failed: " + e);
            return false;
    @Override
    protected String getTestPackage() {
        return TEST_PACKAGE;
    }

    @Override
    protected String getTestActivity() {
        return TEST_ACTIVITY;
    }

    private void waitForProcessFreeze(int pid, long timeoutMs) {
        // TODO: Add a listener to monitor freezer state changes.
        SystemUtil.runWithShellPermissionIdentity(() -> {
            TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
                    (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
                    () -> mActivityManager.isProcessFrozen(pid));
        });
    @Override
    protected void putExtra(Intent intent) {
        intent.putExtra(TEST_DISPLAYS, mDisplayCount);
    }

    private void waitForProcessUnfreeze(int pid, long timeoutMs) {
        // TODO: Add a listener to monitor freezer state changes.
        SystemUtil.runWithShellPermissionIdentity(() -> {
            TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
                    (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
                    () -> !mActivityManager.isProcessFrozen(pid));
        });
    /**
     * Return a display bundle at the stated index.  The bundle is retrieved under lock.
     */
    private DisplayBundle displayBundleAt(int i) {
        synchronized (mLock) {
            return mDisplayBundles.valueAt(i);
        }
    }

    /**
     * Create virtual displays, change their configurations and release them. The number of
     * displays is set by the {@link #mDisplays} variable.
     * displays is set by the {@link #data()} parameter.
     */
    private void testDisplayEventsInternal(boolean cached, boolean frozen) {
        Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + cached + " " + frozen);
@@ -444,110 +379,6 @@ public class DisplayEventDeliveryTest {
        testDisplayEventsInternal(true, true);
    }

    /**
     * Launch the test activity that would listen to display events. Return its process ID.
     */
    private int launchTestActivity() {
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY);
        intent.putExtra(TEST_MESSENGER, mMessenger);
        intent.putExtra(TEST_DISPLAYS, mDisplayCount);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        SystemUtil.runWithShellPermissionIdentity(
                () -> {
                    mContext.startActivity(intent);
                },
                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
        waitLatch(mLatchActivityLaunch);

        try {
            String cmd = "pidof " + TEST_PACKAGE;
            String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
            return Integer.parseInt(result.trim());
        } catch (IOException e) {
            fail("failed to get pid of test package");
            return 0;
        } catch (NumberFormatException e) {
            fail("failed to parse pid " + e);
            return 0;
        }
    }

    /**
     * Bring the test activity back to top
     */
    private void bringTestActivityTop() {
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        SystemUtil.runWithShellPermissionIdentity(
                () -> {
                    mContext.startActivity(intent);
                },
                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
    }

    /**
     * Bring the test activity into cached mode by launching another 2 apps
     */
    private void makeTestActivityCached() {
        // Launch another activity to bring the test activity into background
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.setClass(mContext, SimpleActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);

        // Launch another activity to bring the test activity into cached mode
        Intent intent2 = new Intent(Intent.ACTION_MAIN);
        intent2.setClass(mContext, SimpleActivity2.class);
        intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        SystemUtil.runWithShellPermissionIdentity(
                () -> {
                    mInstrumentation.startActivitySync(intent);
                    mInstrumentation.startActivitySync(intent2);
                },
                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
        waitLatch(mLatchActivityCached);
    }

    // Sleep, ignoring interrupts.
    private void pause(int s) {
        try { Thread.sleep(s * 1000); } catch (Exception e) { }
    }

    /**
     * Freeze the test activity.
     */
    private void makeTestActivityFrozen(int pid) {
        // The delay here is meant to allow pending binder transactions to drain.  A process
        // cannot be frozen if it has pending binder transactions, and attempting to freeze such a
        // process more than a few times will result in the system killing the process.
        pause(5);
        try {
            String cmd = "am freeze --sticky ";
            SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
        } catch (IOException e) {
            fail(e.toString());
        }
        // Wait for the freeze to complete in the kernel and for the frozen process
        // notification to settle out.
        waitForProcessFreeze(pid, 5 * 1000);
    }

    /**
     * Freeze the test activity.
     */
    private void makeTestActivityUnfrozen(int pid) {
        try {
            String cmd = "am unfreeze --sticky ";
            SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
        } catch (IOException e) {
            fail(e.toString());
        }
        // Wait for the freeze to complete in the kernel and for the frozen process
        // notification to settle out.
        waitForProcessUnfreeze(pid, 5 * 1000);
    }

    /**
     * Create a virtual display
     *
@@ -560,15 +391,4 @@ public class DisplayEventDeliveryTest {
                VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
                /* flags: a public virtual display that another app can access */);
    }

    /**
     * Wait for CountDownLatch with timeout
     */
    private void waitLatch(CountDownLatch latch) {
        try {
            latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
Loading