diff --git a/api/current.txt b/api/current.txt index e6bacc0419855..f840b8a15503d 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4844,6 +4844,7 @@ package android.app { public class Instrumentation { ctor public Instrumentation(); + method public android.os.TestLooperManager acquireLooperManager(android.os.Looper); method public void addMonitor(android.app.Instrumentation.ActivityMonitor); method public android.app.Instrumentation.ActivityMonitor addMonitor(android.content.IntentFilter, android.app.Instrumentation.ActivityResult, boolean); method public android.app.Instrumentation.ActivityMonitor addMonitor(java.lang.String, android.app.Instrumentation.ActivityResult, boolean); @@ -31556,6 +31557,16 @@ package android.os { method public static long uptimeMillis(); } + public class TestLooperManager { + method public void execute(android.os.Message); + method public android.os.MessageQueue getQueue(); + method public boolean hasMessages(android.os.Handler, java.lang.Object, int); + method public boolean hasMessages(android.os.Handler, java.lang.Object, java.lang.Runnable); + method public android.os.Message next(); + method public void recycle(android.os.Message); + method public void release(); + } + public abstract class TokenWatcher { ctor public TokenWatcher(android.os.Handler, java.lang.String); method public void acquire(android.os.IBinder, java.lang.String); diff --git a/api/system-current.txt b/api/system-current.txt index 173aaf3db2622..a93459990ca93 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5013,6 +5013,7 @@ package android.app { public class Instrumentation { ctor public Instrumentation(); + method public android.os.TestLooperManager acquireLooperManager(android.os.Looper); method public void addMonitor(android.app.Instrumentation.ActivityMonitor); method public android.app.Instrumentation.ActivityMonitor addMonitor(android.content.IntentFilter, android.app.Instrumentation.ActivityResult, boolean); method public android.app.Instrumentation.ActivityMonitor addMonitor(java.lang.String, android.app.Instrumentation.ActivityResult, boolean); @@ -34317,6 +34318,16 @@ package android.os { method public static long uptimeMillis(); } + public class TestLooperManager { + method public void execute(android.os.Message); + method public android.os.MessageQueue getQueue(); + method public boolean hasMessages(android.os.Handler, java.lang.Object, int); + method public boolean hasMessages(android.os.Handler, java.lang.Object, java.lang.Runnable); + method public android.os.Message next(); + method public void recycle(android.os.Message); + method public void release(); + } + public abstract class TokenWatcher { ctor public TokenWatcher(android.os.Handler, java.lang.String); method public void acquire(android.os.IBinder, java.lang.String); diff --git a/api/test-current.txt b/api/test-current.txt index 99d960ba3c3f7..208308ef5190f 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -4854,6 +4854,7 @@ package android.app { public class Instrumentation { ctor public Instrumentation(); + method public android.os.TestLooperManager acquireLooperManager(android.os.Looper); method public void addMonitor(android.app.Instrumentation.ActivityMonitor); method public android.app.Instrumentation.ActivityMonitor addMonitor(android.content.IntentFilter, android.app.Instrumentation.ActivityResult, boolean); method public android.app.Instrumentation.ActivityMonitor addMonitor(java.lang.String, android.app.Instrumentation.ActivityResult, boolean); @@ -31679,6 +31680,16 @@ package android.os { method public static long uptimeMillis(); } + public class TestLooperManager { + method public void execute(android.os.Message); + method public android.os.MessageQueue getQueue(); + method public boolean hasMessages(android.os.Handler, java.lang.Object, int); + method public boolean hasMessages(android.os.Handler, java.lang.Object, java.lang.Runnable); + method public android.os.Message next(); + method public void recycle(android.os.Message); + method public void release(); + } + public abstract class TokenWatcher { ctor public TokenWatcher(android.os.Handler, java.lang.String); method public void acquire(android.os.IBinder, java.lang.String); diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 4db29fb5a72ac..f9a3ea74a40fa 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -37,6 +37,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; +import android.os.TestLooperManager; import android.os.UserHandle; import android.util.AndroidRuntimeException; import android.util.Log; @@ -109,6 +110,22 @@ public class Instrumentation { public Instrumentation() { } + /** + * Called for methods that shouldn't be called by standard apps and + * should only be used in instrumentation environments. This is not + * security feature as these classes will still be accessible through + * reflection, but it will serve as noticeable discouragement from + * doing such a thing. + */ + private void checkInstrumenting(String method) { + // Check if we have an instrumentation context, as init should only get called by + // the system in startup processes that are being instrumented. + if (mInstrContext == null) { + throw new RuntimeException(method + + " cannot be called outside of instrumented processes"); + } + } + /** * Called when the instrumentation is starting, before any application code * has been loaded. Usually this will be implemented to simply call @@ -2024,6 +2041,15 @@ public class Instrumentation { return null; } + /** + * Takes control of the execution of messages on the specified looper until + * {@link TestLooperManager#release} is called. + */ + public TestLooperManager acquireLooperManager(Looper looper) { + checkInstrumenting("acquireLooperManager"); + return new TestLooperManager(looper); + } + private final class InstrumentationThread extends Thread { public InstrumentationThread(String name) { super(name); diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java new file mode 100644 index 0000000000000..745642efc4232 --- /dev/null +++ b/core/java/android/os/TestLooperManager.java @@ -0,0 +1,209 @@ +/* + * 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. + *

+ * 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 sHeldLoopers = new ArraySet<>(); + + private final MessageQueue mQueue; + private final Looper mLooper; + private final LinkedBlockingQueue 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 getQueue() { + checkReleased(); + return mQueue; + } + + /** + * Returns the next message that should be executed by this queue, may block + * if no messages are ready. + *

+ * 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. + *

+ * 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; + } +}