1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
15package android.os;
16
17import android.util.ArraySet;
18
19import java.util.concurrent.LinkedBlockingQueue;
20
21/**
22 * Blocks a looper from executing any messages, and allows the holder of this object
23 * to control when and which messages get executed until it is released.
24 * <p>
25 * A TestLooperManager should be acquired using
26 * {@link android.app.Instrumentation#acquireLooperManager}. Until {@link #release()} is called,
27 * the Looper thread will not execute any messages except when {@link #execute(Message)} is called.
28 * The test code may use {@link #next()} to acquire messages that have been queued to this
29 * {@link Looper}/{@link MessageQueue} and then {@link #execute} to run any that desires.
30 */
31public class TestLooperManager {
32
33    private static final ArraySet<Looper> sHeldLoopers = new ArraySet<>();
34
35    private final MessageQueue mQueue;
36    private final Looper mLooper;
37    private final LinkedBlockingQueue<MessageExecution> mExecuteQueue = new LinkedBlockingQueue<>();
38
39    private boolean mReleased;
40    private boolean mLooperBlocked;
41
42    /**
43     * @hide
44     */
45    public TestLooperManager(Looper looper) {
46        synchronized (sHeldLoopers) {
47            if (sHeldLoopers.contains(looper)) {
48                throw new RuntimeException("TestLooperManager already held for this looper");
49            }
50            sHeldLoopers.add(looper);
51        }
52        mLooper = looper;
53        mQueue = mLooper.getQueue();
54        // Post a message that will keep the looper blocked as long as we are dispatching.
55        new Handler(looper).post(new LooperHolder());
56    }
57
58    /**
59     * Returns the {@link MessageQueue} this object is wrapping.
60     */
61    public MessageQueue getMessageQueue() {
62        checkReleased();
63        return mQueue;
64    }
65
66    /** @removed */
67    @Deprecated
68    public MessageQueue getQueue() {
69        return getMessageQueue();
70    }
71
72    /**
73     * Returns the next message that should be executed by this queue, may block
74     * if no messages are ready.
75     * <p>
76     * Callers should always call {@link #recycle(Message)} on the message when all
77     * interactions with it have completed.
78     */
79    public Message next() {
80        // Wait for the looper block to come up, to make sure we don't accidentally get
81        // the message for the block.
82        while (!mLooperBlocked) {
83            synchronized (this) {
84                try {
85                    wait();
86                } catch (InterruptedException e) {
87                }
88            }
89        }
90        checkReleased();
91        return mQueue.next();
92    }
93
94    /**
95     * Releases the looper to continue standard looping and processing of messages,
96     * no further interactions with TestLooperManager will be allowed after
97     * release() has been called.
98     */
99    public void release() {
100        synchronized (sHeldLoopers) {
101            sHeldLoopers.remove(mLooper);
102        }
103        checkReleased();
104        mReleased = true;
105        mExecuteQueue.add(new MessageExecution());
106    }
107
108    /**
109     * Executes the given message on the Looper thread this wrapper is
110     * attached to.
111     * <p>
112     * Execution will happen on the Looper's thread (whether it is the current thread
113     * or not), but all RuntimeExceptions encountered while executing the message will
114     * be thrown on the calling thread.
115     */
116    public void execute(Message message) {
117        checkReleased();
118        if (Looper.myLooper() == mLooper) {
119            // This is being called from the thread it should be executed on, we can just dispatch.
120            message.target.dispatchMessage(message);
121        } else {
122            MessageExecution execution = new MessageExecution();
123            execution.m = message;
124            synchronized (execution) {
125                mExecuteQueue.add(execution);
126                // Wait for the message to be executed.
127                try {
128                    execution.wait();
129                } catch (InterruptedException e) {
130                }
131                if (execution.response != null) {
132                    throw new RuntimeException(execution.response);
133                }
134            }
135        }
136    }
137
138    /**
139     * Called to indicate that a Message returned by {@link #next()} has been parsed
140     * and should be recycled.
141     */
142    public void recycle(Message msg) {
143        checkReleased();
144        msg.recycleUnchecked();
145    }
146
147    /**
148     * Returns true if there are any queued messages that match the parameters.
149     *
150     * @param h      the value of {@link Message#getTarget()}
151     * @param what   the value of {@link Message#what}
152     * @param object the value of {@link Message#obj}, null for any
153     */
154    public boolean hasMessages(Handler h, Object object, int what) {
155        checkReleased();
156        return mQueue.hasMessages(h, what, object);
157    }
158
159    /**
160     * Returns true if there are any queued messages that match the parameters.
161     *
162     * @param h      the value of {@link Message#getTarget()}
163     * @param r      the value of {@link Message#getCallback()}
164     * @param object the value of {@link Message#obj}, null for any
165     */
166    public boolean hasMessages(Handler h, Object object, Runnable r) {
167        checkReleased();
168        return mQueue.hasMessages(h, r, object);
169    }
170
171    private void checkReleased() {
172        if (mReleased) {
173            throw new RuntimeException("release() has already be called");
174        }
175    }
176
177    private class LooperHolder implements Runnable {
178        @Override
179        public void run() {
180            synchronized (TestLooperManager.this) {
181                mLooperBlocked = true;
182                TestLooperManager.this.notify();
183            }
184            while (!mReleased) {
185                try {
186                    final MessageExecution take = mExecuteQueue.take();
187                    if (take.m != null) {
188                        processMessage(take);
189                    }
190                } catch (InterruptedException e) {
191                }
192            }
193            synchronized (TestLooperManager.this) {
194                mLooperBlocked = false;
195            }
196        }
197
198        private void processMessage(MessageExecution mex) {
199            synchronized (mex) {
200                try {
201                    mex.m.target.dispatchMessage(mex.m);
202                    mex.response = null;
203                } catch (Throwable t) {
204                    mex.response = t;
205                }
206                mex.notifyAll();
207            }
208        }
209    }
210
211    private static class MessageExecution {
212        private Message m;
213        private Throwable response;
214    }
215}
216