Skip to content

Commit

Permalink
Merge pull request #6144 from robolectric/piper_350815564
Browse files Browse the repository at this point in the history
Add support for MessageQueue IdleHandlers to ShadowPausedMessageQueue. This allows ShadowPausedLooper.idle() and ShadowPausedLooper.runOneTask() to trigger them.
  • Loading branch information
hoisie committed Jan 16, 2021
2 parents 69a7550 + d7cdc6b commit a3d94c2
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 2 deletions.
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
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
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 a3d94c2

Please sign in to comment.