Skip to content

Instantly share code, notes, and snippets.

@daj
Last active August 29, 2015 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daj/3a6600702a4e9af8a934 to your computer and use it in GitHub Desktop.
Save daj/3a6600702a4e9af8a934 to your computer and use it in GitHub Desktop.
Example IdlingResource for use with the JobManager in https://github.com/yigit/android-priority-jobqueue
/**
* Used by Espresso tests to tell when it should wait because some background work needs to be done.
*
* Based on advice from the priority-job-manager maintainer:
* https://github.com/path/android-priority-jobqueue/issues/49#issuecomment-45583487
*/
public class JobManagerIdlingResource implements IdlingResource {
private static final String TAG = "JobManagerIdlingResource";
private final CBEngineContext mCBEngineContext;
private final JobManager mJobManager;
private ResourceCallback mResourceCallback;
private IdleCheckJob mIdleCheckJob;
public JobManagerIdlingResource(CBEngineContext cbEngineContext) {
mCBEngineContext = cbEngineContext;
mJobManager = cbEngineContext.getJobManager();
}
@Override
public String getName() {
return JobManagerIdlingResource.class.getName();
}
@Override
public boolean isIdleNow() {
if (CBBaseJob.getRunningJobCount() == 0) {
return true;
} else {
if (mResourceCallback != null) {
if (mIdleCheckJob == null) {
Log.d(TAG, "Not currently idle, no watcher in job queue, add one now");
addIdleWatcherJob(mResourceCallback);
} else {
// Don't add another watcher as one is already in the queue
Log.d(TAG, "Not currently idle, watcher already in job queue");
}
}
return false;
}
}
@Override
public void registerIdleTransitionCallback(final ResourceCallback callback) {
mResourceCallback = callback;
if (!isIdleNow()) {
addIdleWatcherJob(callback);
}
}
private long addIdleWatcherJob(final ResourceCallback callback) {
mIdleCheckJob = new IdleCheckJob(mCBEngineContext, callback);
return mJobManager.addJob(mIdleCheckJob);
}
/**
* IdlingResource requires us to give an asynchronous callback to Espresso to tell it when the
* application is idle. The easiest way to do this is to add a job to the queue, and recheck
* the idle state when the job completes (the alternative is polling, which nobody wants to
* see).
*
* This implementation is based on:
* https://github.com/dpreussler/android-gluten/blob/master/src/main/java/de/jodamob/android/espresso/PriorityJobQueueIdleMonitor.java
*/
private final class IdleCheckJob extends CBBaseJob {
private final ResourceCallback callback;
private static final long serialVersionUID = -7531425496860713384L;
private IdleCheckJob(CBEngineContext cbEngineContext, ResourceCallback callback) {
// Ensure the checker job has the lowest priority so it gives all other jobs a chance
// to finish first
super(cbEngineContext, CBRestApiProtobuf.PRIORITY_LOW);
this.callback = callback;
}
@Override
public void onAdded() { }
@Override
protected void onCancel() {
mIdleCheckJob = null;
notifyOrReAdd(callback);
}
@Override
public void onRun() {
mIdleCheckJob = null;
// If we DID call super.onRun(), this is the only time we would want to call it BEFORE
// doing our work (since notifyOrReAdd needs to check the queue count)
notifyOrReAdd(callback);
}
private void notifyOrReAdd(final ResourceCallback callback) {
if (isIdleNow()) {
LogIt.d(TAG, "Now idle, call onTransitionToIdle");
callback.onTransitionToIdle();
} else {
addIdleWatcherJob(callback);
}
}
@Override
protected boolean shouldReRunOnThrowable(Throwable arg0) {
return false;
}
}
}
/**
* My real test extends my own base test class, which is an AndroidTestCase. I've tried
* to only show the most interesting parts of the code here.
*/
public class JobManagerIdlingResourceTest extends AndroidTestCase implements IdlingResource.ResourceCallback {
protected CountDownLatch mLatch = new CountDownLatch(1);
protected BaseMockRestApi mMockApi;
protected JobManagerIdlingResource mIdlingResourceMonitor;
private boolean mDidCallbackHappen;
@Override
protected void setUp() throws Exception {
super.setUp();
mDidCallbackHappen = false;
mMockApi = new MockRestApi200EmptyProtobufSuccess();
mCBEngineContext = CBEngineContext.getTestCBEngineContext();
mIdlingResourceMonitor = new JobManagerIdlingResource(mCBEngineContext);
mIdlingResourceMonitor.registerIdleTransitionCallback(this);
}
public void testTransitionToIdleCalled() {
resetLatch();
mMockApi.setLatch(mLatch);
setMockRestApi(mMockApi);
assertTrue(mIdlingResourceMonitor.isIdleNow());
assertFalse(mDidCallbackHappen);
CBBaseJob job = new IngredientsSearchJob(mCBEngineContext, "blah");
mCBEngineContext.getJobManager().addJob(job);
assertFalse(mIdlingResourceMonitor.isIdleNow());
assertFalse(mDidCallbackHappen);
// Now release the latch to let it finish
mLatch.countDown();
// Wait for the callback to happen
Condition condition = new Condition() {
@Override
public boolean isSatisfied() {
return mDidCallbackHappen == true;
}
};
assertCondition(condition);
}
@Override
public void onTransitionToIdle() {
mDidCallbackHappen = true;
}
}
/**
* All my JobManager jobs extend this base class.
*/
public abstract class MyBaseJob extends Job {
protected CBEngineContext mCBEngineContext;
private static AtomicInteger sRunningJobCount = new AtomicInteger(0);
/**
* @return the number of jobs that are waiting to run, or are running in the JobManager.
*/
public static int getRunningJobCount() {
return sRunningJobCount.get();
}
public MyBaseJob(CBEngineContext cbEngineContext) {
// We do not set our REST jobs to requireNetwork() as otherwise our jobs will not be
// started until a network is available. For all user initiated actions, we want them
// to fail quickly if there is no network.
super(new Params(MyConstants.PRIORITY_HIGH));
mCBEngineContext = cbEngineContext;
}
/**
* Subclasses must call super.onAdded() BEFORE doing anything else.
*/
@Override
public void onAdded() {
sRunningJobCount.incrementAndGet();
}
/**
* Subclasses must call super.onRun() AFTER completing all their steps!
*/
@Override
public void onRun() {
sRunningJobCount.decrementAndGet();
}
@Override
protected boolean shouldReRunOnThrowable(Throwable throwable) {
// Do not retry if an error occurs in onRun (this will trigger a call to onCancel)
return false;
}
/**
* Subclasses must call super.onCancel() AFTER completing all their steps!
*/
@Override
protected void onCancel() {
// Job has exceeded retry attempts or shouldReRunOnThrowable() returned false
sRunningJobCount.decrementAndGet();
}
}
@xCatG
Copy link

xCatG commented May 31, 2015

The biggest caveat is all the super.onRun() calls in the derived job classes need to be called at the end of the call to ensure running job count is decreased.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment