blob: 5e7549fa67d83000f8670466256391669dfeb24f [file] [log] [blame]
/*
* Copyright (C) 2017 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
* License 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 android.os;
import android.util.ArraySet;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Blocks a looper from executing any messages, and allows the holder of this object
* to control when and which messages get executed until it is released.
* <p>
* A TestLooperManager should be acquired using
* {@link android.app.Instrumentation#acquireLooperManager}. Until {@link #release()} is called,
* the Looper thread will not execute any messages except when {@link #execute(Message)} is called.
* The test code may use {@link #next()} to acquire messages that have been queued to this
* {@link Looper}/{@link MessageQueue} and then {@link #execute} to run any that desires.
*/
public class TestLooperManager {
private static final ArraySet<Looper> sHeldLoopers = new ArraySet<>();
private final MessageQueue mQueue;
private final Looper mLooper;
private final LinkedBlockingQueue<MessageExecution> mExecuteQueue = new LinkedBlockingQueue<>();
private boolean mReleased;
private boolean mLooperBlocked;
/**
* @hide
*/
public TestLooperManager(Looper looper) {
synchronized (sHeldLoopers) {
if (sHeldLoopers.contains(looper)) {
throw new RuntimeException("TestLooperManager already held for this looper");
}
sHeldLoopers.add(looper);
}
mLooper = looper;
mQueue = mLooper.getQueue();
// Post a message that will keep the looper blocked as long as we are dispatching.
new Handler(looper).post(new LooperHolder());
}
/**
* Returns the {@link MessageQueue} this object is wrapping.
*/
public MessageQueue getMessageQueue() {
checkReleased();
return mQueue;
}
/** @removed */
@Deprecated
public MessageQueue getQueue() {
return getMessageQueue();
}
/**
* Returns the next message that should be executed by this queue, may block
* if no messages are ready.
* <p>
* Callers should always call {@link #recycle(Message)} on the message when all
* interactions with it have completed.
*/
public Message next() {
// Wait for the looper block to come up, to make sure we don't accidentally get
// the message for the block.
while (!mLooperBlocked) {
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
checkReleased();
return mQueue.next();
}
/**
* Releases the looper to continue standard looping and processing of messages,
* no further interactions with TestLooperManager will be allowed after
* release() has been called.
*/
public void release() {
synchronized (sHeldLoopers) {
sHeldLoopers.remove(mLooper);
}
checkReleased();
mReleased = true;
mExecuteQueue.add(new MessageExecution());
}
/**
* Executes the given message on the Looper thread this wrapper is
* attached to.
* <p>
* Execution will happen on the Looper's thread (whether it is the current thread
* or not), but all RuntimeExceptions encountered while executing the message will
* be thrown on the calling thread.
*/
public void execute(Message message) {
checkReleased();
if (Looper.myLooper() == mLooper) {
// This is being called from the thread it should be executed on, we can just dispatch.
message.target.dispatchMessage(message);
} else {
MessageExecution execution = new MessageExecution();
execution.m = message;
synchronized (execution) {
mExecuteQueue.add(execution);
// Wait for the message to be executed.
try {
execution.wait();
} catch (InterruptedException e) {
}
if (execution.response != null) {
throw new RuntimeException(execution.response);
}
}
}
}
/**
* Called to indicate that a Message returned by {@link #next()} has been parsed
* and should be recycled.
*/
public void recycle(Message msg) {
checkReleased();
msg.recycleUnchecked();
}
/**
* Returns true if there are any queued messages that match the parameters.
*
* @param h the value of {@link Message#getTarget()}
* @param what the value of {@link Message#what}
* @param object the value of {@link Message#obj}, null for any
*/
public boolean hasMessages(Handler h, Object object, int what) {
checkReleased();
return mQueue.hasMessages(h, what, object);
}
/**
* Returns true if there are any queued messages that match the parameters.
*
* @param h the value of {@link Message#getTarget()}
* @param r the value of {@link Message#getCallback()}
* @param object the value of {@link Message#obj}, null for any
*/
public boolean hasMessages(Handler h, Object object, Runnable r) {
checkReleased();
return mQueue.hasMessages(h, r, object);
}
private void checkReleased() {
if (mReleased) {
throw new RuntimeException("release() has already be called");
}
}
private class LooperHolder implements Runnable {
@Override
public void run() {
synchronized (TestLooperManager.this) {
mLooperBlocked = true;
TestLooperManager.this.notify();
}
while (!mReleased) {
try {
final MessageExecution take = mExecuteQueue.take();
if (take.m != null) {
processMessage(take);
}
} catch (InterruptedException e) {
}
}
synchronized (TestLooperManager.this) {
mLooperBlocked = false;
}
}
private void processMessage(MessageExecution mex) {
synchronized (mex) {
try {
mex.m.target.dispatchMessage(mex.m);
mex.response = null;
} catch (Throwable t) {
mex.response = t;
}
mex.notifyAll();
}
}
}
private static class MessageExecution {
private Message m;
private Throwable response;
}
}