Skip to content

Commit

Permalink
Add support for MessageQueue IdleHandlers to ShadowPausedLooper and S…
Browse files Browse the repository at this point in the history
…hadowPausedMessageQueue

This allows ShadowPausedLooper.idle(), ShadowPausedLooper.idleFor(), and
ShadowPausedLooper.runOneTask() to trigger them.

PiperOrigin-RevId: 350815564
  • Loading branch information
Googler authored and hoisie committed Jan 16, 2021
1 parent 232c4ce commit d7cdc6b
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.time.Duration;
Expand Down Expand Up @@ -196,6 +198,145 @@ public void idle_executesTask_mainLooper() {
verify(mockRunnable, times(1)).run();
}

@Test
public void idle_executesTask_andIdleHandler() {
ShadowPausedLooper shadowLooper = Shadow.extract(getMainLooper());
Runnable mockRunnable = mock(Runnable.class);
IdleHandler mockIdleHandler = mock(IdleHandler.class);
getMainLooper().getQueue().addIdleHandler(mockIdleHandler);
Handler mainHandler = new Handler();
mainHandler.post(mockRunnable);
verify(mockRunnable, times(0)).run();
verify(mockIdleHandler, times(0)).queueIdle();

shadowLooper.idle();
verify(mockRunnable, times(1)).run();
verify(mockIdleHandler, times(1)).queueIdle();
}

@Test
public void idle_executesTask_andIdleHandler_removesIdleHandler() {
ShadowPausedLooper shadowLooper = Shadow.extract(getMainLooper());
Runnable mockRunnable = mock(Runnable.class);
IdleHandler mockIdleHandler = mock(IdleHandler.class);
getMainLooper().getQueue().addIdleHandler(mockIdleHandler);
Handler mainHandler = new Handler();
mainHandler.post(mockRunnable);
verify(mockRunnable, times(0)).run();
verify(mockIdleHandler, times(0)).queueIdle();

shadowLooper.idle();
verify(mockRunnable, times(1)).run();
verify(mockIdleHandler, times(1)).queueIdle();

mainHandler.post(mockRunnable);
shadowLooper.idle();
verify(mockRunnable, times(2)).run();
verify(mockIdleHandler, times(1)).queueIdle(); // It was not kept, does not run again.
}

@Test
public void idle_executesTask_andIdleHandler_keepsIdleHandler() {
ShadowPausedLooper shadowLooper = Shadow.extract(getMainLooper());
Runnable mockRunnable = mock(Runnable.class);
IdleHandler mockIdleHandler = mock(IdleHandler.class);
when(mockIdleHandler.queueIdle()).thenReturn(true);
getMainLooper().getQueue().addIdleHandler(mockIdleHandler);
Handler mainHandler = new Handler();
mainHandler.post(mockRunnable);
verify(mockRunnable, times(0)).run();
verify(mockIdleHandler, times(0)).queueIdle();

shadowLooper.idle();
verify(mockRunnable, times(1)).run();
verify(mockIdleHandler, times(1)).queueIdle();

mainHandler.post(mockRunnable);
shadowLooper.idle();
verify(mockRunnable, times(2)).run();
verify(mockIdleHandler, times(2)).queueIdle(); // It was kept and runs again
}

@Test
public void runOneTask_executesTask_andIdleHandler() {
ShadowPausedLooper shadowLooper = Shadow.extract(getMainLooper());
Runnable mockRunnable = mock(Runnable.class);
IdleHandler mockIdleHandler = mock(IdleHandler.class);
getMainLooper().getQueue().addIdleHandler(mockIdleHandler);
Handler mainHandler = new Handler();
mainHandler.post(mockRunnable);
verify(mockRunnable, times(0)).run();
verify(mockIdleHandler, times(0)).queueIdle();

shadowLooper.runOneTask();
verify(mockRunnable, times(1)).run();
verify(mockIdleHandler, times(1)).queueIdle();
}

@Test
public void runOneTask_executesTwoTasks_thenIdleHandler() {
ShadowPausedLooper shadowLooper = Shadow.extract(getMainLooper());
Runnable mockRunnable = mock(Runnable.class);
IdleHandler mockIdleHandler = mock(IdleHandler.class);
getMainLooper().getQueue().addIdleHandler(mockIdleHandler);
Handler mainHandler = new Handler();
mainHandler.post(mockRunnable);
mainHandler.post(mockRunnable);
verify(mockRunnable, times(0)).run();
verify(mockIdleHandler, times(0)).queueIdle();

shadowLooper.runOneTask();
verify(mockRunnable, times(1)).run();
verify(mockIdleHandler, times(0)).queueIdle();

shadowLooper.runOneTask();
verify(mockRunnable, times(2)).run();
verify(mockIdleHandler, times(1)).queueIdle();
}

@Test
public void runOneTask_executesTask_andIdleHandler_removesIdleHandler() {
ShadowPausedLooper shadowLooper = Shadow.extract(getMainLooper());
Runnable mockRunnable = mock(Runnable.class);
IdleHandler mockIdleHandler = mock(IdleHandler.class);
getMainLooper().getQueue().addIdleHandler(mockIdleHandler);
Handler mainHandler = new Handler();
mainHandler.post(mockRunnable);
verify(mockRunnable, times(0)).run();
verify(mockIdleHandler, times(0)).queueIdle();

shadowLooper.runOneTask();
verify(mockRunnable, times(1)).run();
verify(mockIdleHandler, times(1)).queueIdle();

mainHandler.post(mockRunnable);
shadowLooper.idle();
verify(mockRunnable, times(2)).run();
verify(mockIdleHandler, times(1)).queueIdle(); // It was not kept, does not run again.
}

@Test
public void runOneTask_executesTask_andIdleHandler_keepsIdleHandler() {
ShadowPausedLooper shadowLooper = Shadow.extract(getMainLooper());
Runnable mockRunnable = mock(Runnable.class);
IdleHandler mockIdleHandler = mock(IdleHandler.class);
when(mockIdleHandler.queueIdle()).thenReturn(true);
getMainLooper().getQueue().addIdleHandler(mockIdleHandler);
Handler mainHandler = new Handler();
mainHandler.post(mockRunnable);
verify(mockRunnable, times(0)).run();
verify(mockIdleHandler, times(0)).queueIdle();

shadowLooper.runOneTask();
verify(mockRunnable, times(1)).run();
verify(mockIdleHandler, times(1)).queueIdle();

mainHandler.post(mockRunnable);
shadowLooper.runOneTask();
verify(mockRunnable, times(2)).run();
verify(mockIdleHandler, times(2)).queueIdle(); // It was kept and runs again
}

@Test
public void idleFor_executesTask_mainLooper() {
ShadowPausedLooper shadowLooper = Shadow.extract(getMainLooper());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
import android.util.Log;
import java.time.Duration;
Expand Down Expand Up @@ -282,6 +283,39 @@ private void setLooperExecutor(Executor executor) {
looperExecutor = executor;
}

/** Retrieves the next message or null if the queue is idle. */
private Message getNextExecutableMessage() {
synchronized (realLooper.getQueue()) {
// Use null if the queue is idle, otherwise getNext() will block.
return shadowQueue().isIdle() ? null : shadowQueue().getNext();
}
}

/**
* If the given {@code lastMessageRead} is not null and the queue is now idle, get the idle
* handlers and run them. This synchronization mirrors what happens in the real message queue
* next() method, but does not block after running the idle handlers.
*/
private void triggerIdleHandlersIfNeeded(Message lastMessageRead) {
List<IdleHandler> idleHandlers;
// Mirror the synchronization of MessageQueue.next(). If a message was read on the last call
// to next() and the queue is now idle, make a copy of the idle handlers and release the lock.
// Run the idle handlers without holding the lock, removing those that return false from their
// queueIdle() method.
synchronized (realLooper.getQueue()) {
if (lastMessageRead == null || !shadowQueue().isIdle()) {
return;
}
idleHandlers = shadowQueue().getIdleHandlersCopy();
}
for (IdleHandler idleHandler : idleHandlers) {
if (!idleHandler.queueIdle()) {
// This method already has synchronization internally.
realLooper.getQueue().removeIdleHandler(idleHandler);
}
}
}

/** A runnable that changes looper state, and that must be run from looper's thread */
private abstract static class ControlRunnable implements Runnable {

Expand All @@ -300,10 +334,14 @@ private class IdlingRunnable extends ControlRunnable {

@Override
public void run() {
while (!shadowQueue().isIdle()) {
Message msg = shadowQueue().getNext();
while (true) {
Message msg = getNextExecutableMessage();
if (msg == null) {
break;
}
msg.getTarget().dispatchMessage(msg);
shadowMsg(msg).recycleUnchecked();
triggerIdleHandlersIfNeeded(msg);
}
runLatch.countDown();
}
Expand All @@ -317,6 +355,7 @@ public void run() {
if (msg != null) {
SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen());
msg.getTarget().dispatchMessage(msg);
triggerIdleHandlersIfNeeded(msg);
}
runLatch.countDown();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,16 @@ public void setHead(Message msg) {
throw new UnsupportedOperationException("Not supported in PAUSED LooperMode.");
}

/**
* Retrieves a copy of the current list of idle handlers. Idle handlers are read with
* synchronization on the real queue.
*/
ArrayList<IdleHandler> getIdleHandlersCopy() {
synchronized (realQueue) {
return new ArrayList<>(reflector(ReflectorMessageQueue.class, realQueue).getIdleHandlers());
}
}

/** Accessor interface for {@link MessageQueue}'s internals. */
@ForType(MessageQueue.class)
private interface ReflectorMessageQueue {
Expand All @@ -334,6 +344,9 @@ private interface ReflectorMessageQueue {
@Accessor("mIdleHandlers")
void setIdleHandlers(ArrayList<IdleHandler> list);

@Accessor("mIdleHandlers")
ArrayList<IdleHandler> getIdleHandlers();

@Accessor("mNextBarrierToken")
void setNextBarrierToken(int token);

Expand Down

0 comments on commit d7cdc6b

Please sign in to comment.