MessageStack: add quitting sentinel node

We use the timestamp of the quitting message to tell
DeliQueue whether it should remove all messages or just
remove messages past a given time ("removeAllFutureMessages").

Test: atest android.os.MessageStackTest
Bug: 421623328
Flag: EXEMPT new data structure isn't used yet; usages will be flagged.
Change-Id: I7caf36e9a5fc1efec1be7f733b4b2d7f2f176f92
diff --git a/core/java/android/os/MessageStack.java b/core/java/android/os/MessageStack.java
index 5424d05..be71ad1 100644
--- a/core/java/android/os/MessageStack.java
+++ b/core/java/android/os/MessageStack.java
@@ -41,7 +41,7 @@
     private final MessageHeap mAsyncHeap = new MessageHeap();
 
     // This points to the most-recently processed message. Comparison with mTopValue will indicate
-    // whether some messages still need to be processed.
+    // whether some messages still need to be processed. This value excludes the quitting sentinel.
     private Message mLooperProcessed = null;
 
     static {
@@ -56,17 +56,65 @@
         }
     }
 
+    private static final Object QUITTING_NODE_OBJ = new Object();
+
+    private boolean isQuittingMessage(Message m) {
+        return m != null && m.obj == QUITTING_NODE_OBJ;
+    }
+
     /**
      * Pushes a message onto the top of the stack with a CAS.
+     * @return true if successfully pushed; false if the stack is quitting.
      */
-    public void pushMessage(Message m) {
-        // TODO: This should fail if the current top value is the shutdown sentinel.
+    public boolean pushMessage(Message m) {
         Message current;
         do {
             current = mTopValue;
+            if (isQuittingMessage(current)) {
+                return false;
+            }
             m.next = current;
         } while (!sTop.weakCompareAndSetRelease(this, current, m));
+        return true;
+    }
 
+    /**
+     * Pushes a quitting message onto the top of the stack.
+     * After this call no more messages can be pushed onto the stack.
+     * @return true if pushed, false if there was already a quitting message
+     */
+    public boolean pushQuitting(long when) {
+        Message quittingMsg = Message.obtain();
+        quittingMsg.obj = QUITTING_NODE_OBJ;
+        quittingMsg.when = when;
+        /* Should never go into the heap, initialize idx to an impossible value */
+        quittingMsg.heapIndex = -1;
+        final boolean ret = pushMessage(quittingMsg);
+        if (!ret) {
+            quittingMsg.recycleUnchecked();
+        }
+
+        return ret;
+    }
+
+    /**
+     * Query if we are in a quitting state.
+     * @return true if we have a quitting message on top of the stack.
+     */
+    public boolean isQuitting() {
+        return isQuittingMessage((Message) sTop.getAcquire(this));
+    }
+
+    /**
+     * Gets timestamp of quitting message.
+     * @return timestamp, or throws an exception if no quitting message exists.
+     */
+    public long getQuittingTimestamp() throws IllegalStateException {
+        Message m = (Message) sTop.getAcquire(this);
+        if (!isQuittingMessage(m)) {
+            throw new IllegalStateException();
+        }
+        return m.when;
     }
 
     /**
@@ -80,7 +128,8 @@
 
         while (current != null) {
             // Check that the message hasn't already been removed or processed elsewhere.
-            if (!current.isRemoved()
+            if (!isQuittingMessage(current)
+                    && !current.isRemoved()
                     && compare.compareMessage(current, h, what, object, r, when)
                     && current.markRemoved()) {
                 if (firstRemoved == null) {
@@ -107,6 +156,14 @@
      */
     public void heapSweep() {
         Message current = (Message) sTop.getAcquire(this);
+
+        if (current != null && isQuittingMessage(current)) {
+            if (current.next != null) {
+                current.next.prev = current;
+            }
+            current = current.next;
+        }
+
         Message prevLooperProcessed = mLooperProcessed;
         mLooperProcessed = current;
 
diff --git a/core/tests/coretests/src/android/os/MessageStackTest.java b/core/tests/coretests/src/android/os/MessageStackTest.java
index 6e4238c..015e0ba 100644
--- a/core/tests/coretests/src/android/os/MessageStackTest.java
+++ b/core/tests/coretests/src/android/os/MessageStackTest.java
@@ -17,7 +17,9 @@
 package android.os;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -46,6 +48,24 @@
     }
 
     /**
+     * Verify quitting state
+     */
+    @Test
+    public void testQuitting() {
+        MessageStack stack = new MessageStack();
+        for (int i = 0; i < 10; i++) {
+            stack.pushMessage(new Message());
+        }
+
+        assertFalse(stack.isQuitting());
+
+        stack.pushQuitting(42);
+        assertFalse(stack.pushMessage(new Message()));
+        assertTrue(stack.isQuitting());
+        assertEquals(stack.getQuittingTimestamp(), 42);
+    }
+
+    /**
      * Verify heap size after sweeping messages from a MessageStack.
      */
     @Test