Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5be50f7d authored by Jason Monk's avatar Jason Monk
Browse files

Method to allow testing of Loopers

Add a wrapper object that provides some control and access to
the package hidden methods on Looper only for the purpose of
testing. Will not work on non-instrumented apps.

Test: none
Change-Id: I55cdfeac17ddc0d251852ab764501544079fc888
parent c9c57d44
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -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);
+11 −0
Original line number Diff line number Diff line
@@ -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);
+11 −0
Original line number Diff line number Diff line
@@ -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);
+26 −0
Original line number Diff line number Diff line
@@ -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);
+209 −0
Original line number Diff line number Diff line
/*
 * 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 getQueue() {
        checkReleased();
        return mQueue;
    }

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