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