1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.wifi;
18
19import static org.junit.Assert.assertTrue;
20
21import android.os.Looper;
22import android.os.Message;
23import android.os.MessageQueue;
24import android.os.SystemClock;
25import android.util.Log;
26
27import java.lang.reflect.Constructor;
28import java.lang.reflect.Field;
29import java.lang.reflect.InvocationTargetException;
30import java.lang.reflect.Method;
31
32/**
33 * Creates a looper whose message queue can be manipulated
34 * This allows testing code that uses a looper to dispatch messages in a deterministic manner
35 * Creating a MockLooper will also install it as the looper for the current thread
36 */
37public class MockLooper {
38    protected final Looper mLooper;
39
40    private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
41    private static final Field THREAD_LOCAL_LOOPER_FIELD;
42    private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
43    private static final Field MESSAGE_NEXT_FIELD;
44    private static final Field MESSAGE_WHEN_FIELD;
45    private static final Method MESSAGE_MARK_IN_USE_METHOD;
46    private static final String TAG = "MockLooper";
47
48    private AutoDispatchThread mAutoDispatchThread;
49
50    static {
51        try {
52            LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
53            LOOPER_CONSTRUCTOR.setAccessible(true);
54            THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
55            THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
56            MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
57            MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
58            MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
59            MESSAGE_NEXT_FIELD.setAccessible(true);
60            MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
61            MESSAGE_WHEN_FIELD.setAccessible(true);
62            MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
63            MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
64        } catch (NoSuchFieldException | NoSuchMethodException e) {
65            throw new RuntimeException("Failed to initialize MockLooper", e);
66        }
67    }
68
69
70    public MockLooper() {
71        try {
72            mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
73
74            ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
75                    .get(null);
76            threadLocalLooper.set(mLooper);
77        } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
78            throw new RuntimeException("Reflection error constructing or accessing looper", e);
79        }
80    }
81
82    public Looper getLooper() {
83        return mLooper;
84    }
85
86    private Message getMessageLinkedList() {
87        try {
88            MessageQueue queue = mLooper.getQueue();
89            return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
90        } catch (IllegalAccessException e) {
91            throw new RuntimeException("Access failed in MockLooper: get - MessageQueue.mMessages",
92                    e);
93        }
94    }
95
96    public void moveTimeForward(long milliSeconds) {
97        try {
98            Message msg = getMessageLinkedList();
99            while (msg != null) {
100                long updatedWhen = msg.getWhen() - milliSeconds;
101                if (updatedWhen < 0) {
102                    updatedWhen = 0;
103                }
104                MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
105                msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
106            }
107        } catch (IllegalAccessException e) {
108            throw new RuntimeException("Access failed in MockLooper: set - Message.when", e);
109        }
110    }
111
112    private Message messageQueueNext() {
113        try {
114            long now = SystemClock.uptimeMillis();
115
116            Message prevMsg = null;
117            Message msg = getMessageLinkedList();
118            if (msg != null && msg.getTarget() == null) {
119                // Stalled by a barrier. Find the next asynchronous message in
120                // the queue.
121                do {
122                    prevMsg = msg;
123                    msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
124                } while (msg != null && !msg.isAsynchronous());
125            }
126            if (msg != null) {
127                if (now >= msg.getWhen()) {
128                    // Got a message.
129                    if (prevMsg != null) {
130                        MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
131                    } else {
132                        MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
133                                MESSAGE_NEXT_FIELD.get(msg));
134                    }
135                    MESSAGE_NEXT_FIELD.set(msg, null);
136                    MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
137                    return msg;
138                }
139            }
140        } catch (IllegalAccessException | InvocationTargetException e) {
141            throw new RuntimeException("Access failed in MockLooper", e);
142        }
143
144        return null;
145    }
146
147    /**
148     * @return true if there are pending messages in the message queue
149     */
150    public synchronized boolean isIdle() {
151        Message messageList = getMessageLinkedList();
152
153        return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen();
154    }
155
156    /**
157     * @return the next message in the Looper's message queue or null if there is none
158     */
159    public synchronized Message nextMessage() {
160        if (isIdle()) {
161            return messageQueueNext();
162        } else {
163            return null;
164        }
165    }
166
167    /**
168     * Dispatch the next message in the queue
169     * Asserts that there is a message in the queue
170     */
171    public synchronized void dispatchNext() {
172        assertTrue(isIdle());
173        Message msg = messageQueueNext();
174        if (msg == null) {
175            return;
176        }
177        msg.getTarget().dispatchMessage(msg);
178    }
179
180    /**
181     * Dispatch all messages currently in the queue
182     * Will not fail if there are no messages pending
183     * @return the number of messages dispatched
184     */
185    public synchronized int dispatchAll() {
186        int count = 0;
187        while (isIdle()) {
188            dispatchNext();
189            ++count;
190        }
191        return count;
192    }
193
194    /**
195     * Thread used to dispatch messages when the main thread is blocked waiting for a response.
196     */
197    private class AutoDispatchThread extends Thread {
198        private static final int MAX_LOOPS = 100;
199        private static final int LOOP_SLEEP_TIME_MS = 10;
200
201        private RuntimeException mAutoDispatchException = null;
202
203        /**
204         * Run method for the auto dispatch thread.
205         * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
206         * The thread continues looping and attempting to dispatch all messages until at
207         * least one message has been dispatched.
208         */
209        @Override
210        public void run() {
211            int dispatchCount = 0;
212            for (int i = 0; i < MAX_LOOPS; i++) {
213                try {
214                    dispatchCount = dispatchAll();
215                } catch (RuntimeException e) {
216                    mAutoDispatchException = e;
217                }
218                Log.d(TAG, "dispatched " + dispatchCount + " messages");
219                if (dispatchCount > 0) {
220                    return;
221                }
222                try {
223                    Thread.sleep(LOOP_SLEEP_TIME_MS);
224                } catch (InterruptedException e) {
225                    mAutoDispatchException = new IllegalStateException(
226                            "stopAutoDispatch called before any messages were dispatched.");
227                    return;
228                }
229            }
230            Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
231            mAutoDispatchException = new IllegalStateException(
232                    "MockLooper did not dispatch any messages before exiting.");
233        }
234
235        /**
236         * Method allowing the MockLooper to pass any exceptions thrown by the thread to be passed
237         * to the main thread.
238         *
239         * @return RuntimeException Exception created by stopping without dispatching a message
240         */
241        public RuntimeException getException() {
242            return mAutoDispatchException;
243        }
244    }
245
246    /**
247     * Create and start a new AutoDispatchThread if one is not already running.
248     */
249    public void startAutoDispatch() {
250        if (mAutoDispatchThread != null) {
251            throw new IllegalStateException(
252                    "startAutoDispatch called with the AutoDispatchThread already running.");
253        }
254        mAutoDispatchThread = new AutoDispatchThread();
255        mAutoDispatchThread.start();
256    }
257
258    /**
259     * If an AutoDispatchThread is currently running, stop and clean up.
260     */
261    public void stopAutoDispatch() {
262        if (mAutoDispatchThread != null) {
263            if (mAutoDispatchThread.isAlive()) {
264                mAutoDispatchThread.interrupt();
265            }
266            try {
267                mAutoDispatchThread.join();
268            } catch (InterruptedException e) {
269                // Catch exception from join.
270            }
271
272            RuntimeException e = mAutoDispatchThread.getException();
273            mAutoDispatchThread = null;
274            if (e != null) {
275                throw e;
276            }
277        } else {
278            // stopAutoDispatch was called when startAutoDispatch has not created a new thread.
279            throw new IllegalStateException(
280                    "stopAutoDispatch called without startAutoDispatch.");
281        }
282    }
283}
284