UriIdlingResource.java
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Lice`nse is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.espresso.idling.net;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.test.espresso.IdlingResource;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* An implementation of {@link IdlingResource} useful for monitoring idleness of network traffic.
*
* <p>This is similar to {@link androidx.test.espresso.contrib.CountingIdlingResource}, with
* the additional idleness constraint that the counter must be 0 for a set period of time before the
* resource becomes idle.
*
* <p>A network timeout is required to be reasonably sure that the webview has finished loading.
* Imagine the case where each response that comes back causes another request to be made until
* loading is complete. The counter will go from 0->1->0->1->0->1..., but we don't want to report
* the webview as idle each time this happens.
*
* <p><b>This API is currently in beta.</b>
*/
public class UriIdlingResource implements IdlingResource {
private static final String TAG = "UriIdlingResource";
private final String resourceName;
private final long timeoutMs;
private final boolean debug;
// Read and modified from multiple threads
private final AtomicInteger counter = new AtomicInteger(0);
private final CopyOnWriteArrayList<Pattern> ignoredRegexes = new CopyOnWriteArrayList<>();
private final AtomicBoolean idle = new AtomicBoolean(true);
private final Runnable transitionToIdle;
private volatile ResourceCallback resourceCallback;
private final HandlerIntf handler;
public UriIdlingResource(String resourceName, long timeoutMs) {
this(resourceName, timeoutMs, false, new DefaultHandler(new Handler(Looper.getMainLooper())));
}
@VisibleForTesting
UriIdlingResource(String resourceName, long timeoutMs, boolean debug, HandlerIntf handler) {
if (timeoutMs <= 0) {
throw new IllegalArgumentException("timeoutMs has to be greater than 0");
}
this.resourceName = resourceName;
this.timeoutMs = timeoutMs;
this.debug = debug;
this.handler = handler;
transitionToIdle =
new Runnable() {
@Override
public void run() {
idle.set(true);
if (resourceCallback != null) {
resourceCallback.onTransitionToIdle();
}
}
};
}
@Override
public String getName() {
return resourceName;
}
@Override
public boolean isIdleNow() {
return idle.get();
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
/**
* Add a regex pattern to the ignore list.
*
* <p>All request URIs are checked against all patterns, and matches are ignored for the purposes
* of detecting when the webview is idle.
*
* <p>Ignored patterns can only be added when the webview is idle.
*/
public void ignoreUri(Pattern pattern) {
if (!isIdleNow()) {
Log.e(TAG, "Ignored patterns can only be added when the resource is idle.");
} else {
ignoredRegexes.add(pattern);
}
}
/**
* Called when a request is made.
*
* <p>If the URI is not blacklisted the idle counter is incremented.
*/
public void beginLoad(String uri) {
if (uriIsIgnored(uri)) {
return;
}
idle.set(false);
long count = counter.getAndIncrement();
if (count == 0) {
handler.removeCallbacks(transitionToIdle);
}
if (debug) {
Log.i(TAG, "Resource " + resourceName + " counter increased to " + (count + 1));
}
}
/**
* Called when a request is completed (ie the response is returned).
*
* <p>If the URI is not blacklisted the idle counter is decremented. Once the idle counter reaches
* 0, the idle update thread will set the resource as idle after the appropriate timeout.
*/
public void endLoad(String uri) {
if (uriIsIgnored(uri)) {
return;
}
int count = counter.decrementAndGet();
if (count < 0) {
throw new IllegalStateException("Counter has been corrupted! Count=" + count);
} else if (count == 0) {
handler.postDelayed(transitionToIdle, timeoutMs);
}
if (debug) {
Log.i(TAG, "Resource " + resourceName + " counter decreased to " + count);
}
}
private boolean uriIsIgnored(String uri) {
for (Pattern pattern : ignoredRegexes) {
if (pattern.matcher(uri).matches()) {
Log.i(TAG, "Resource " + resourceName + " ignored URI: <" + uri + ">");
return true;
}
}
return false;
}
@VisibleForTesting
void forceIdleTransition() {
transitionToIdle.run();
}
/**
* Wraps a Handler object.
*
* <p>Mock this for testing purposes.
*/
public static interface HandlerIntf {
public void postDelayed(Runnable runnable, long millis);
public void removeCallbacks(Runnable runnable);
}
private static final class DefaultHandler implements HandlerIntf {
private final Handler handler;
public DefaultHandler(Handler handler) {
this.handler = handler;
}
@Override
public void postDelayed(Runnable runnable, long millis) {
handler.postDelayed(runnable, millis);
}
@Override
public void removeCallbacks(Runnable runnable) {
handler.removeCallbacks(runnable);
}
}
}