summaryrefslogtreecommitdiff
path: root/android/os/TestLooperManager.java
blob: 5e7549fa67d83000f8670466256391669dfeb24f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/*
 * 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;
    }
}