Skip to content

Commit

Permalink
Add new shadow Looper APIs for retrieving the scheduled time for tasks.
Browse files Browse the repository at this point in the history
Also mark all ShadowRealistic* APIs as Beta.

PiperOrigin-RevId: 241634871
  • Loading branch information
brettchabot authored and copybara-robolectric committed Apr 3, 2019
1 parent 8e1e585 commit 6494bde
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 4 deletions.
Expand Up @@ -10,8 +10,10 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
Expand Down Expand Up @@ -454,6 +456,26 @@ public void isIdle() {
assertThat(shadowMainLooper().isIdle()).isTrue();
}

@Test
public void getNextScheduledTime() {
ShadowLooper.pauseMainLooper();
assertThat(shadowMainLooper().getNextScheduledTaskTime()).isEqualTo(Duration.ZERO);
Handler mainHandler = new Handler();
mainHandler.postDelayed(() -> {}, 100);
assertThat(shadowMainLooper().getNextScheduledTaskTime().toMillis()).isEqualTo(
SystemClock.uptimeMillis() + 100);
}

@Test
public void getLastScheduledTime() {
ShadowLooper.pauseMainLooper();
assertThat(shadowMainLooper().getLastScheduledTaskTime()).isEqualTo(Duration.ZERO);
Handler mainHandler = new Handler();
mainHandler.postDelayed(() -> {}, 200);
mainHandler.postDelayed(() -> {}, 100);
assertThat(shadowMainLooper().getLastScheduledTaskTime().toMillis()).isEqualTo(SystemClock.uptimeMillis() + 200);
}

@After
public void tearDown() {
RoboSettings.setUseGlobalScheduler(false);
Expand Down
Expand Up @@ -15,6 +15,7 @@
import android.os.Looper;
import android.os.SystemClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -197,6 +198,23 @@ public void isIdle() {
assertThat(shadowMainLooper().isIdle()).isTrue();
}

@Test
public void getNextScheduledTime() {
assertThat(shadowMainLooper().getNextScheduledTaskTime()).isEqualTo(Duration.ZERO);
Handler mainHandler = new Handler();
mainHandler.postDelayed(() -> {}, 100);
assertThat(shadowMainLooper().getNextScheduledTaskTime().toMillis()).isEqualTo(SystemClock.uptimeMillis() + 100);
}

@Test
public void getLastScheduledTime() {
assertThat(shadowMainLooper().getLastScheduledTaskTime()).isEqualTo(Duration.ZERO);
Handler mainHandler = new Handler();
mainHandler.postDelayed(() -> {}, 200);
mainHandler.postDelayed(() -> {}, 100);
assertThat(shadowMainLooper().getLastScheduledTaskTime().toMillis()).isEqualTo(SystemClock.uptimeMillis() + 200);
}

@Before
public void assertMainLooperEmpty() {
assertThat(ShadowRealisticLooper.isMainLooperIdle()).isTrue();
Expand Down
@@ -1,6 +1,7 @@
package org.robolectric.shadows;

import android.os.Looper;
import androidx.test.annotation.Beta;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.robolectric.annotation.LooperMode;
Expand All @@ -11,7 +12,10 @@
* The base API class for controlling Loopers.
*
* It will delegate calls to the appropriate shadow based on the current LooperMode.
*
* Beta API, subject to change
*/
@Beta
public abstract class ShadowBaseLooper {

/**
Expand Down Expand Up @@ -44,7 +48,8 @@ public void idleFor(Duration duration) {
/**
* Runs the current task with the looper paused.
*
* When LooperMode is PAUSED, this will execute all pending
* When LooperMode is PAUSED, this will execute all pending tasks scheduled before the current
* time.
*/
public abstract void runPaused(Runnable run);

Expand All @@ -57,7 +62,10 @@ public void idleFor(Duration duration) {
public abstract void idleIfPaused();

/**
* Returns true if looper is currently idle.
* Returns true if looper has no pending tasks which are scheduled for execution at or before
* current time.
*
* Note this does NOT necessarily mean looper is not currently busy executing a task.
*/
public abstract boolean isIdle();

Expand All @@ -68,6 +76,18 @@ public void idleFor(Duration duration) {
*/
public abstract void pause();

/**
* @return the scheduled time of the next posted task; Duration.ZERO if there is no currently
* scheduled task.
*/
public abstract Duration getNextScheduledTaskTime();

/**
* @return the scheduled time of the last posted task; Duration.ZERO 0 if there is
* no currently scheduled task.
*/
public abstract Duration getLastScheduledTaskTime();

public static ShadowBaseLooper shadowMainLooper() {
return Shadow.extract(Looper.getMainLooper());
}
Expand Down
Expand Up @@ -7,6 +7,7 @@

import android.os.Looper;
import android.os.MessageQueue;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
Expand Down Expand Up @@ -361,6 +362,16 @@ public void pause() {
getScheduler().pause();
}

@Override
public Duration getNextScheduledTaskTime() {
return getScheduler().getNextScheduledTaskTime();
}

@Override
public Duration getLastScheduledTaskTime() {
return getScheduler().getLastScheduledTaskTime();
}

public void unPause() {
getScheduler().unPause();
}
Expand Down
Expand Up @@ -3,18 +3,26 @@
import static org.robolectric.shadow.api.Shadow.invokeConstructor;

import android.os.AsyncTask;
import androidx.test.annotation.Beta;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;

/**
* A new variant of a AsyncTask shadow that is active when {@link
* ShadowBaseLooper#useRealisticLooper()} is enabled.
*
* This is beta API, and will very likely be renamed in a future Robolectric release.
*/
@Implements(
value = AsyncTask.class,
shadowPicker = ShadowBaseAsyncTask.Picker.class,
// TODO: turn off shadowOf generation. Figure out why this is needed
isInAndroidSdk = false)
@Beta
public class ShadowRealisticAsyncTask<Params, Progress, Result> extends ShadowBaseAsyncTask {

@RealObject private AsyncTask<Params, Progress, Result> realObject;
Expand Down
Expand Up @@ -8,6 +8,8 @@
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import androidx.test.annotation.Beta;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -33,13 +35,17 @@
* that thread. -
* - There is only a single {@link SystemClock} value that all loopers read from. Unlike legacy
* behavior where each {@link org.robolectric.util.Scheduler} kept their own clock value.
*
* This is beta API, and will very likely be renamed in a future Robolectric release.
* Its recommended to use ShadowBaseLooper instead of this type directly.
*/
@Implements(
value = Looper.class,
shadowPicker = ShadowBaseLooper.Picker.class,
// TODO: turn off shadowOf generation. Figure out why this is needed
isInAndroidSdk = false)
@SuppressWarnings("NewApi")
@Beta
public class ShadowRealisticLooper extends ShadowBaseLooper {

// Keep reference to all created Loopers so they can be torn down after test
Expand Down Expand Up @@ -104,6 +110,18 @@ public void pause() {
}
}

@Override
public Duration getNextScheduledTaskTime() {
ShadowRealisticMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue());
return shadowQueue.getNextScheduledTaskTime();
}

@Override
public Duration getLastScheduledTaskTime() {
ShadowRealisticMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue());
return shadowQueue.getLastScheduledTaskTime();
}

public static boolean isMainLooperIdle() {
Looper mainLooper = Looper.getMainLooper();
if (mainLooper != null) {
Expand Down
Expand Up @@ -45,13 +45,20 @@ long getWhen() {
return reflector(ReflectorMessage.class, realObject).getWhen();
}

Message getNext() {
return reflector(ReflectorMessage.class, realObject).getNext();
}

/** Accessor interface for {@link Message}'s internals. */
@ForType(Message.class)
private interface ReflectorMessage {

@Accessor("when")
long getWhen();

@Accessor("next")
Message getNext();

@Static
@Accessor("sPool")
void setPool(Message o);
Expand All @@ -61,5 +68,6 @@ private interface ReflectorMessage {
void setPoolSize(int size);

void recycleUnchecked();

}
}
Expand Up @@ -16,6 +16,8 @@
import android.os.MessageQueue;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
import androidx.test.annotation.Beta;
import java.time.Duration;
import java.util.ArrayList;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
Expand All @@ -26,12 +28,19 @@
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;

/**
* * A new variant of a MessageQueue shadow that is active when {@link
* * ShadowBaseLooper#useRealisticLooper()} is enabled.
*
* This is beta API, and will very likely be renamed in a future Robolectric release.
*/
@Implements(
value = MessageQueue.class,
shadowPicker = ShadowBaseMessageQueue.Picker.class,
// TODO: turn off shadowOf generation. Figure out why this is needed
isInAndroidSdk = false,
looseSignatures = true)
@Beta
public class ShadowRealisticMessageQueue extends ShadowBaseMessageQueue {

@RealObject private MessageQueue realQueue;
Expand Down Expand Up @@ -141,8 +150,7 @@ public boolean isIdle() {
if (headMsg == null) {
return true;
}
ShadowRealisticMessage shadowMsg = Shadow.extract(headMsg);
long when = shadowMsg.getWhen();
long when = shadowOfMsg(headMsg).getWhen();
return now < when;
}
}
Expand Down Expand Up @@ -211,6 +219,30 @@ private static int getInt(Object intOrLongObj) {
}
}

Duration getNextScheduledTaskTime() {
Message head = getMessages();
if (head == null) {
return Duration.ZERO;
}
return Duration.ofMillis(shadowOfMsg(head).getWhen());
}

Duration getLastScheduledTaskTime() {
long when = 0;
synchronized (realQueue) {
Message next = getMessages();
while (next != null) {
when = shadowOfMsg(next).getWhen();
next = shadowOfMsg(next).getNext();
}
}
return Duration.ofMillis(when);
}

private static ShadowRealisticMessage shadowOfMsg(Message head) {
return Shadow.extract(head);
}

/** Accessor interface for {@link MessageQueue}'s internals. */
@ForType(MessageQueue.class)
private interface ReflectorMessageQueue {
Expand Down
Expand Up @@ -4,6 +4,7 @@
import static android.os.Build.VERSION_CODES.P;

import android.os.SystemClock;
import androidx.test.annotation.Beta;
import java.time.DateTimeException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
Expand All @@ -23,11 +24,14 @@
*
* <p>{@link SystemClock#uptimeMillis()} and {@link SystemClock#currentThreadTimeMillis()} are
* identical.
*
* This is beta API, and will very likely be renamed in a future Robolectric release.
*/
@Implements(
value = SystemClock.class,
isInAndroidSdk = false,
shadowPicker = ShadowBaseSystemClock.Picker.class)
@Beta
public class ShadowRealisticSystemClock extends ShadowBaseSystemClock {
private static final long INITIAL_TIME = 100;
private static final int MILLIS_PER_NANO = 1000000;;
Expand Down
21 changes: 21 additions & 0 deletions utils/src/main/java/org/robolectric/util/Scheduler.java
Expand Up @@ -4,6 +4,7 @@
import static org.robolectric.util.Scheduler.IdleState.PAUSED;
import static org.robolectric.util.Scheduler.IdleState.UNPAUSED;

import java.time.Duration;
import java.util.Iterator;
import java.util.PriorityQueue;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -309,6 +310,26 @@ public synchronized int size() {
return runnables.size();
}

@SuppressWarnings("AndroidJdkLibsChecker")
public synchronized Duration getNextScheduledTaskTime() {
return runnables.isEmpty() ? Duration.ZERO : Duration.ofMillis(runnables.peek().scheduledTime);
}

@SuppressWarnings("AndroidJdkLibsChecker")
public synchronized Duration getLastScheduledTaskTime() {
if (runnables.isEmpty()) {
return Duration.ZERO;
}
long currentMaxTime = currentTime;
for (ScheduledRunnable scheduled : runnables) {
if (currentMaxTime < scheduled.scheduledTime) {
currentMaxTime = scheduled.scheduledTime;
}

}
return Duration.ofMillis(currentMaxTime);
}

/**
* Set the idle state of the Scheduler. If necessary, the clock will be advanced and runnables
* executed as required by the newly-set state.
Expand Down

0 comments on commit 6494bde

Please sign in to comment.