OSDN Git Service

Create frameworks-base-testutils lib
authorChristopher Wiley <wiley@google.com>
Thu, 19 May 2016 00:09:56 +0000 (17:09 -0700)
committerChristopher Wiley <wiley@google.com>
Tue, 7 Jun 2016 17:00:08 +0000 (10:00 -0700)
( cherry-pick 07630f6bd3dbc91e2622cedfd1a21822b5f1f4b5 )

This permits sharing of test components used in connectivity tests.
Rename the MockLooper to TestLooper, reflecting that it is not a
true mock, but a manually controlled Looper for use in tests.

Bug: 28848133
Test: Tests consuming this library pass.

Change-Id: I57e11f9544c7bffdb02739ab2a921512b1b11874

tests/utils/testutils/Android.mk [new file with mode: 0644]
tests/utils/testutils/java/android/app/test/MockAnswerUtil.java [new file with mode: 0644]
tests/utils/testutils/java/android/app/test/TestAlarmManager.java [new file with mode: 0644]
tests/utils/testutils/java/android/os/test/TestLooper.java [new file with mode: 0644]
tests/utils/testutils/java/android/os/test/TestLooperTest.java [new file with mode: 0644]
tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java [new file with mode: 0644]
tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java [new file with mode: 0644]

diff --git a/tests/utils/testutils/Android.mk b/tests/utils/testutils/Android.mk
new file mode 100644 (file)
index 0000000..d53167f
--- /dev/null
@@ -0,0 +1,30 @@
+#
+# Copyright (C) 2016 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := frameworks-base-testutils
+LOCAL_MODULE_TAG := tests
+
+LOCAL_SRC_FILES := $(call all-java-files-under,java)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    mockito-target
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java b/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java
new file mode 100644 (file)
index 0000000..746c77d
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 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.app.test;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * Utilities for creating Answers for mock objects
+ */
+public class MockAnswerUtil {
+
+    /**
+     * Answer that calls the method in the Answer called "answer" that matches the type signature of
+     * the method being answered. An error will be thrown at runtime if the signature does not match
+     * exactly.
+     */
+    public static class AnswerWithArguments implements Answer<Object> {
+        @Override
+        public final Object answer(InvocationOnMock invocation) throws Throwable {
+            Method method = invocation.getMethod();
+            try {
+                Method implementation = getClass().getMethod("answer", method.getParameterTypes());
+                if (!implementation.getReturnType().equals(method.getReturnType())) {
+                    throw new RuntimeException("Found answer method does not have expected return "
+                            + "type. Expected: " + method.getReturnType() + ", got "
+                            + implementation.getReturnType());
+                }
+                Object[] args = invocation.getArguments();
+                try {
+                    return implementation.invoke(this, args);
+                } catch (IllegalAccessException e) {
+                    throw new RuntimeException("Error invoking answer method", e);
+                } catch (InvocationTargetException e) {
+                    throw e.getCause();
+                }
+            } catch (NoSuchMethodException e) {
+                throw new RuntimeException("Could not find answer method with the expected args "
+                        + Arrays.toString(method.getParameterTypes()), e);
+            }
+        }
+    }
+
+}
diff --git a/tests/utils/testutils/java/android/app/test/TestAlarmManager.java b/tests/utils/testutils/java/android/app/test/TestAlarmManager.java
new file mode 100644 (file)
index 0000000..e90ea1e
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2015 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.app.test;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import android.app.AlarmManager;
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Creates an AlarmManager whose alarm dispatch can be controlled
+ * Currently only supports alarm listeners
+ *
+ * Alarm listeners will be dispatched to the handler provided or will
+ * be dispatched immediately if they would have been sent to the main
+ * looper (handler was null).
+ */
+public class TestAlarmManager {
+    private final AlarmManager mAlarmManager;
+    private final List<PendingAlarm> mPendingAlarms;
+
+    public TestAlarmManager() throws Exception {
+        mPendingAlarms = new ArrayList<>();
+
+        mAlarmManager = mock(AlarmManager.class);
+        doAnswer(new SetListenerAnswer()).when(mAlarmManager).set(anyInt(), anyLong(), anyString(),
+                any(AlarmManager.OnAlarmListener.class), any(Handler.class));
+        doAnswer(new SetListenerAnswer()).when(mAlarmManager).setExact(anyInt(), anyLong(),
+                anyString(), any(AlarmManager.OnAlarmListener.class), any(Handler.class));
+        doAnswer(new CancelListenerAnswer())
+                .when(mAlarmManager).cancel(any(AlarmManager.OnAlarmListener.class));
+    }
+
+    public AlarmManager getAlarmManager() {
+        return mAlarmManager;
+    }
+
+    /**
+     * Dispatch a pending alarm with the given tag
+     * @return if any alarm was dispatched
+     */
+    public boolean dispatch(String tag) {
+        for (int i = 0; i < mPendingAlarms.size(); ++i) {
+            PendingAlarm alarm = mPendingAlarms.get(i);
+            if (Objects.equals(tag, alarm.getTag())) {
+                mPendingAlarms.remove(i);
+                alarm.dispatch();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return if an alarm with the given tag is pending
+     */
+    public boolean isPending(String tag) {
+        for (int i = 0; i < mPendingAlarms.size(); ++i) {
+            PendingAlarm alarm = mPendingAlarms.get(i);
+            if (Objects.equals(tag, alarm.getTag())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return trigger time of an pending alarm with the given tag
+     *         -1 if no pending alarm with the given tag
+     */
+    public long getTriggerTimeMillis(String tag) {
+        for (int i = 0; i < mPendingAlarms.size(); ++i) {
+            PendingAlarm alarm = mPendingAlarms.get(i);
+            if (Objects.equals(tag, alarm.getTag())) {
+                return alarm.getTriggerTimeMillis();
+            }
+        }
+        return -1;
+    }
+
+    private static class PendingAlarm {
+        private final int mType;
+        private final long mTriggerAtMillis;
+        private final String mTag;
+        private final Runnable mCallback;
+
+        public PendingAlarm(int type, long triggerAtMillis, String tag, Runnable callback) {
+            mType = type;
+            mTriggerAtMillis = triggerAtMillis;
+            mTag = tag;
+            mCallback = callback;
+        }
+
+        public void dispatch() {
+            if (mCallback != null) {
+                mCallback.run();
+            }
+        }
+
+        public Runnable getCallback() {
+            return mCallback;
+        }
+
+        public String getTag() {
+            return mTag;
+        }
+
+        public long getTriggerTimeMillis() {
+            return mTriggerAtMillis;
+        }
+    }
+
+    private class SetListenerAnswer extends AnswerWithArguments {
+        public void answer(int type, long triggerAtMillis, String tag,
+                AlarmManager.OnAlarmListener listener, Handler handler) {
+            mPendingAlarms.add(new PendingAlarm(type, triggerAtMillis, tag,
+                            new AlarmListenerRunnable(listener, handler)));
+        }
+    }
+
+    private class CancelListenerAnswer extends AnswerWithArguments {
+        public void answer(AlarmManager.OnAlarmListener listener) {
+            Iterator<PendingAlarm> alarmItr = mPendingAlarms.iterator();
+            while (alarmItr.hasNext()) {
+                PendingAlarm alarm = alarmItr.next();
+                if (alarm.getCallback() instanceof AlarmListenerRunnable) {
+                    AlarmListenerRunnable alarmCallback =
+                            (AlarmListenerRunnable) alarm.getCallback();
+                    if (alarmCallback.getListener() == listener) {
+                        alarmItr.remove();
+                    }
+                }
+            }
+        }
+    }
+
+    private static class AlarmListenerRunnable implements Runnable {
+        private final AlarmManager.OnAlarmListener mListener;
+        private final Handler mHandler;
+        public AlarmListenerRunnable(AlarmManager.OnAlarmListener listener, Handler handler) {
+            mListener = listener;
+            mHandler = handler;
+        }
+
+        public AlarmManager.OnAlarmListener getListener() {
+            return mListener;
+        }
+
+        @Override
+        public void run() {
+            if (mHandler != null) {
+                mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mListener.onAlarm();
+                        }
+                    });
+            } else { // normally gets dispatched in main looper
+                mListener.onAlarm();
+            }
+        }
+    }
+}
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
new file mode 100644 (file)
index 0000000..e8ceb4a
--- /dev/null
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2015 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.test;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Creates a looper whose message queue can be manipulated
+ * This allows testing code that uses a looper to dispatch messages in a deterministic manner
+ * Creating a TestLooper will also install it as the looper for the current thread
+ */
+public class TestLooper {
+    protected final Looper mLooper;
+
+    private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
+    private static final Field THREAD_LOCAL_LOOPER_FIELD;
+    private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
+    private static final Field MESSAGE_NEXT_FIELD;
+    private static final Field MESSAGE_WHEN_FIELD;
+    private static final Method MESSAGE_MARK_IN_USE_METHOD;
+    private static final String TAG = "TestLooper";
+
+    private AutoDispatchThread mAutoDispatchThread;
+
+    static {
+        try {
+            LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
+            LOOPER_CONSTRUCTOR.setAccessible(true);
+            THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
+            THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
+            MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
+            MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
+            MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
+            MESSAGE_NEXT_FIELD.setAccessible(true);
+            MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
+            MESSAGE_WHEN_FIELD.setAccessible(true);
+            MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
+            MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
+        } catch (NoSuchFieldException | NoSuchMethodException e) {
+            throw new RuntimeException("Failed to initialize TestLooper", e);
+        }
+    }
+
+
+    public TestLooper() {
+        try {
+            mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
+
+            ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
+                    .get(null);
+            threadLocalLooper.set(mLooper);
+        } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
+            throw new RuntimeException("Reflection error constructing or accessing looper", e);
+        }
+    }
+
+    public Looper getLooper() {
+        return mLooper;
+    }
+
+    private Message getMessageLinkedList() {
+        try {
+            MessageQueue queue = mLooper.getQueue();
+            return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
+                    e);
+        }
+    }
+
+    public void moveTimeForward(long milliSeconds) {
+        try {
+            Message msg = getMessageLinkedList();
+            while (msg != null) {
+                long updatedWhen = msg.getWhen() - milliSeconds;
+                if (updatedWhen < 0) {
+                    updatedWhen = 0;
+                }
+                MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
+                msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
+            }
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
+        }
+    }
+
+    private Message messageQueueNext() {
+        try {
+            long now = SystemClock.uptimeMillis();
+
+            Message prevMsg = null;
+            Message msg = getMessageLinkedList();
+            if (msg != null && msg.getTarget() == null) {
+                // Stalled by a barrier. Find the next asynchronous message in
+                // the queue.
+                do {
+                    prevMsg = msg;
+                    msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
+                } while (msg != null && !msg.isAsynchronous());
+            }
+            if (msg != null) {
+                if (now >= msg.getWhen()) {
+                    // Got a message.
+                    if (prevMsg != null) {
+                        MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
+                    } else {
+                        MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
+                                MESSAGE_NEXT_FIELD.get(msg));
+                    }
+                    MESSAGE_NEXT_FIELD.set(msg, null);
+                    MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
+                    return msg;
+                }
+            }
+        } catch (IllegalAccessException | InvocationTargetException e) {
+            throw new RuntimeException("Access failed in TestLooper", e);
+        }
+
+        return null;
+    }
+
+    /**
+     * @return true if there are pending messages in the message queue
+     */
+    public synchronized boolean isIdle() {
+        Message messageList = getMessageLinkedList();
+
+        return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen();
+    }
+
+    /**
+     * @return the next message in the Looper's message queue or null if there is none
+     */
+    public synchronized Message nextMessage() {
+        if (isIdle()) {
+            return messageQueueNext();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Dispatch the next message in the queue
+     * Asserts that there is a message in the queue
+     */
+    public synchronized void dispatchNext() {
+        assertTrue(isIdle());
+        Message msg = messageQueueNext();
+        if (msg == null) {
+            return;
+        }
+        msg.getTarget().dispatchMessage(msg);
+    }
+
+    /**
+     * Dispatch all messages currently in the queue
+     * Will not fail if there are no messages pending
+     * @return the number of messages dispatched
+     */
+    public synchronized int dispatchAll() {
+        int count = 0;
+        while (isIdle()) {
+            dispatchNext();
+            ++count;
+        }
+        return count;
+    }
+
+    /**
+     * Thread used to dispatch messages when the main thread is blocked waiting for a response.
+     */
+    private class AutoDispatchThread extends Thread {
+        private static final int MAX_LOOPS = 100;
+        private static final int LOOP_SLEEP_TIME_MS = 10;
+
+        private RuntimeException mAutoDispatchException = null;
+
+        /**
+         * Run method for the auto dispatch thread.
+         * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
+         * The thread continues looping and attempting to dispatch all messages until at
+         * least one message has been dispatched.
+         */
+        @Override
+        public void run() {
+            int dispatchCount = 0;
+            for (int i = 0; i < MAX_LOOPS; i++) {
+                try {
+                    dispatchCount = dispatchAll();
+                } catch (RuntimeException e) {
+                    mAutoDispatchException = e;
+                }
+                Log.d(TAG, "dispatched " + dispatchCount + " messages");
+                if (dispatchCount > 0) {
+                    return;
+                }
+                try {
+                    Thread.sleep(LOOP_SLEEP_TIME_MS);
+                } catch (InterruptedException e) {
+                    mAutoDispatchException = new IllegalStateException(
+                            "stopAutoDispatch called before any messages were dispatched.");
+                    return;
+                }
+            }
+            Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
+            mAutoDispatchException = new IllegalStateException(
+                    "TestLooper did not dispatch any messages before exiting.");
+        }
+
+        /**
+         * Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
+         * to the main thread.
+         *
+         * @return RuntimeException Exception created by stopping without dispatching a message
+         */
+        public RuntimeException getException() {
+            return mAutoDispatchException;
+        }
+    }
+
+    /**
+     * Create and start a new AutoDispatchThread if one is not already running.
+     */
+    public void startAutoDispatch() {
+        if (mAutoDispatchThread != null) {
+            throw new IllegalStateException(
+                    "startAutoDispatch called with the AutoDispatchThread already running.");
+        }
+        mAutoDispatchThread = new AutoDispatchThread();
+        mAutoDispatchThread.start();
+    }
+
+    /**
+     * If an AutoDispatchThread is currently running, stop and clean up.
+     */
+    public void stopAutoDispatch() {
+        if (mAutoDispatchThread != null) {
+            if (mAutoDispatchThread.isAlive()) {
+                mAutoDispatchThread.interrupt();
+            }
+            try {
+                mAutoDispatchThread.join();
+            } catch (InterruptedException e) {
+                // Catch exception from join.
+            }
+
+            RuntimeException e = mAutoDispatchThread.getException();
+            mAutoDispatchThread = null;
+            if (e != null) {
+                throw e;
+            }
+        } else {
+            // stopAutoDispatch was called when startAutoDispatch has not created a new thread.
+            throw new IllegalStateException(
+                    "stopAutoDispatch called without startAutoDispatch.");
+        }
+    }
+}
diff --git a/tests/utils/testutils/java/android/os/test/TestLooperTest.java b/tests/utils/testutils/java/android/os/test/TestLooperTest.java
new file mode 100644 (file)
index 0000000..40d83b5
--- /dev/null
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2016 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.test;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test TestLooperAbstractTime which provides control over "time". Note that
+ * real-time is being used as well. Therefore small time increments are NOT
+ * reliable. All tests are in "K" units (i.e. *1000).
+ */
+
+@SmallTest
+public class TestLooperTest {
+    private TestLooper mTestLooper;
+    private Handler mHandler;
+    private Handler mHandlerSpy;
+
+    @Rule
+    public ErrorCollector collector = new ErrorCollector();
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mTestLooper = new TestLooper();
+        mHandler = new Handler(mTestLooper.getLooper());
+        mHandlerSpy = spy(mHandler);
+    }
+
+    /**
+     * Basic test with no time stamps: dispatch 4 messages, check that all 4
+     * delivered (in correct order).
+     */
+    @Test
+    public void testNoTimeMovement() {
+        final int messageA = 1;
+        final int messageB = 2;
+        final int messageC = 3;
+
+        InOrder inOrder = inOrder(mHandlerSpy);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageC));
+        mTestLooper.dispatchAll();
+
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("2: messageA", messageA, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("3: messageB", messageB, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("4: messageC", messageC, equalTo(messageCaptor.getValue().what));
+
+        inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+    }
+
+    /**
+     * Test message sequence: A, B, C@5K, A@10K. Don't move time.
+     * <p>
+     * Expected: only get A, B
+     */
+    @Test
+    public void testDelayedDispatchNoTimeMove() {
+        final int messageA = 1;
+        final int messageB = 2;
+        final int messageC = 3;
+
+        InOrder inOrder = inOrder(mHandlerSpy);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 10000);
+        mTestLooper.dispatchAll();
+
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+        inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+    }
+
+    /**
+     * Test message sequence: A, B, C@5K, A@10K, Advance time by 5K.
+     * <p>
+     * Expected: only get A, B, C
+     */
+    @Test
+    public void testDelayedDispatchAdvanceTimeOnce() {
+        final int messageA = 1;
+        final int messageB = 2;
+        final int messageC = 3;
+
+        InOrder inOrder = inOrder(mHandlerSpy);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 10000);
+        mTestLooper.moveTimeForward(5000);
+        mTestLooper.dispatchAll();
+
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+
+        inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+    }
+
+    /**
+     * Test message sequence: A, B, C@5K, Advance time by 4K, A@1K, B@2K Advance
+     * time by 1K.
+     * <p>
+     * Expected: get A, B, C, A
+     */
+    @Test
+    public void testDelayedDispatchAdvanceTimeTwice() {
+        final int messageA = 1;
+        final int messageB = 2;
+        final int messageC = 3;
+
+        InOrder inOrder = inOrder(mHandlerSpy);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+        mTestLooper.moveTimeForward(4000);
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 1000);
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+        mTestLooper.moveTimeForward(1000);
+        mTestLooper.dispatchAll();
+
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("4: messageA", messageA, equalTo(messageCaptor.getValue().what));
+
+        inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+    }
+
+    /**
+     * Test message sequence: A, B, C@5K, Advance time by 4K, A@5K, B@2K Advance
+     * time by 3K.
+     * <p>
+     * Expected: get A, B, C, B
+     */
+    @Test
+    public void testDelayedDispatchReverseOrder() {
+        final int messageA = 1;
+        final int messageB = 2;
+        final int messageC = 3;
+
+        InOrder inOrder = inOrder(mHandlerSpy);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+        mTestLooper.moveTimeForward(4000);
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 5000);
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+        mTestLooper.moveTimeForward(3000);
+        mTestLooper.dispatchAll();
+
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("4: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+        inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+    }
+
+    /**
+     * Test message sequence: A, B, C@5K, Advance time by 4K, dispatch all,
+     * A@5K, B@2K Advance time by 3K, dispatch all.
+     * <p>
+     * Expected: get A, B after first dispatch; then C, B after second dispatch
+     */
+    @Test
+    public void testDelayedDispatchAllMultipleTimes() {
+        final int messageA = 1;
+        final int messageB = 2;
+        final int messageC = 3;
+
+        InOrder inOrder = inOrder(mHandlerSpy);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+        mTestLooper.moveTimeForward(4000);
+        mTestLooper.dispatchAll();
+
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 5000);
+        mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+        mTestLooper.moveTimeForward(3000);
+        mTestLooper.dispatchAll();
+
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+        inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+        collector.checkThat("4: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+        inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+    }
+
+    /**
+     * Test AutoDispatch for a single message.
+     * This test would ideally use the Channel sendMessageSynchronously.  At this time, the setup to
+     * get a working test channel is cumbersome.  Until this is fixed, we substitute with a
+     * sendMessage followed by a blocking call.  The main test thread blocks until the test handler
+     * receives the test message (messageA) and sets a boolean true.  Once the boolean is true, the
+     * main thread will exit the busy wait loop, stop autoDispatch and check the assert.
+     *
+     * Enable AutoDispatch, add message, block on message being handled and stop AutoDispatch.
+     * <p>
+     * Expected: handleMessage is called for messageA and stopAutoDispatch is called.
+     */
+    @Test
+    public void testAutoDispatchWithSingleMessage() {
+        final int mLoopSleepTimeMs = 5;
+
+        final int messageA = 1;
+
+        TestLooper mockLooper = new TestLooper();
+        class TestHandler extends Handler {
+            public volatile boolean handledMessage = false;
+            TestHandler(Looper looper) {
+                super(looper);
+            }
+
+            @Override
+            public void handleMessage(Message msg) {
+                if (msg.what == messageA) {
+                    handledMessage = true;
+                }
+            }
+        }
+
+        TestHandler testHandler = new TestHandler(mockLooper.getLooper());
+        mockLooper.startAutoDispatch();
+        testHandler.sendMessage(testHandler.obtainMessage(messageA));
+        while (!testHandler.handledMessage) {
+            // Block until message is handled
+            try {
+                Thread.sleep(mLoopSleepTimeMs);
+            } catch (InterruptedException e) {
+                // Interrupted while sleeping.
+            }
+        }
+        mockLooper.stopAutoDispatch();
+        assertTrue("TestHandler should have received messageA", testHandler.handledMessage);
+    }
+
+    /**
+     * Test starting AutoDispatch while already running throws IllegalStateException
+     * Enable AutoDispatch two times in a row.
+     * <p>
+     * Expected: catch IllegalStateException on second call.
+     */
+    @Test(expected = IllegalStateException.class)
+    public void testRepeatedStartAutoDispatchThrowsException() {
+        mTestLooper.startAutoDispatch();
+        mTestLooper.startAutoDispatch();
+    }
+
+    /**
+     * Test stopping AutoDispatch without previously starting throws IllegalStateException
+     * Stop AutoDispatch
+     * <p>
+     * Expected: catch IllegalStateException on second call.
+     */
+    @Test(expected = IllegalStateException.class)
+    public void testStopAutoDispatchWithoutStartThrowsException() {
+        mTestLooper.stopAutoDispatch();
+    }
+
+    /**
+     * Test AutoDispatch exits and does not dispatch a later message.
+     * Start and stop AutoDispatch then add a message.
+     * <p>
+     * Expected: After AutoDispatch is stopped, dispatchAll will return 1.
+     */
+    @Test
+    public void testAutoDispatchStopsCleanlyWithoutDispatchingAMessage() {
+        final int messageA = 1;
+
+        InOrder inOrder = inOrder(mHandlerSpy);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+        mTestLooper.startAutoDispatch();
+        try {
+            mTestLooper.stopAutoDispatch();
+        } catch (IllegalStateException e) {
+            //  Stopping without a dispatch will throw an exception.
+        }
+
+        mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+        assertEquals("One message should be dispatched", 1, mTestLooper.dispatchAll());
+    }
+
+    /**
+     * Test AutoDispatch throws an exception when no messages are dispatched.
+     * Start and stop AutoDispatch
+     * <p>
+     * Expected: Exception is thrown with the stopAutoDispatch call.
+     */
+    @Test(expected = IllegalStateException.class)
+    public void testAutoDispatchThrowsExceptionWhenNoMessagesDispatched() {
+        mTestLooper.startAutoDispatch();
+        mTestLooper.stopAutoDispatch();
+    }
+}
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java
new file mode 100644 (file)
index 0000000..25cd5b9
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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 com.android.internal.util.test;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+
+
+/**
+ * Provides an AsyncChannel interface that implements the connection initiating half of a
+ * bidirectional channel as described in {@link com.android.internal.util.AsyncChannel}.
+ */
+public class BidirectionalAsyncChannel {
+    private static final String TAG = "BidirectionalAsyncChannel";
+
+    private AsyncChannel mChannel;
+    public enum ChannelState { DISCONNECTED, HALF_CONNECTED, CONNECTED, FAILURE };
+    private ChannelState mState = ChannelState.DISCONNECTED;
+
+    public void assertConnected() {
+        assertEquals("AsyncChannel was not fully connected", ChannelState.CONNECTED, mState);
+    }
+
+    public void connect(final Looper looper, final Messenger messenger,
+            final Handler incomingMessageHandler) {
+        assertEquals("AsyncChannel must be disconnected to connect",
+                ChannelState.DISCONNECTED, mState);
+        mChannel = new AsyncChannel();
+        Handler rawMessageHandler = new Handler(looper) {
+                @Override
+                public void handleMessage(Message msg) {
+                    switch (msg.what) {
+                    case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
+                        if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
+                            Log.d(TAG, "Successfully half connected " + this);
+                            mChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
+                            mState = ChannelState.HALF_CONNECTED;
+                        } else {
+                            Log.d(TAG, "Failed to connect channel " + this);
+                            mState = ChannelState.FAILURE;
+                            mChannel = null;
+                        }
+                        break;
+                    case AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED:
+                        mState = ChannelState.CONNECTED;
+                        Log.d(TAG, "Channel fully connected" + this);
+                        break;
+                    case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+                        mState = ChannelState.DISCONNECTED;
+                        mChannel = null;
+                        Log.d(TAG, "Channel disconnected" + this);
+                        break;
+                    default:
+                        incomingMessageHandler.handleMessage(msg);
+                        break;
+                    }
+                }
+            };
+        mChannel.connect(null, rawMessageHandler, messenger);
+    }
+
+    public void disconnect() {
+        assertEquals("AsyncChannel must be connected to disconnect",
+                ChannelState.CONNECTED, mState);
+        mChannel.sendMessage(AsyncChannel.CMD_CHANNEL_DISCONNECT);
+        mState = ChannelState.DISCONNECTED;
+        mChannel = null;
+    }
+
+    public void sendMessage(Message msg) {
+        assertEquals("AsyncChannel must be connected to send messages",
+                ChannelState.CONNECTED, mState);
+        mChannel.sendMessage(msg);
+    }
+}
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java
new file mode 100644 (file)
index 0000000..49c8332
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 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 com.android.internal.util.test;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides an interface for the server side implementation of a bidirectional channel as described
+ * in {@link com.android.internal.util.AsyncChannel}.
+ */
+public class BidirectionalAsyncChannelServer {
+
+    private static final String TAG = "BidirectionalAsyncChannelServer";
+
+    // Keeps track of incoming clients, which are identifiable by their messengers.
+    private final Map<Messenger, AsyncChannel> mClients = new HashMap<>();
+
+    private Messenger mMessenger;
+
+    public BidirectionalAsyncChannelServer(final Context context, final Looper looper,
+            final Handler messageHandler) {
+        Handler handler = new Handler(looper) {
+            @Override
+            public void handleMessage(Message msg) {
+                AsyncChannel channel = mClients.get(msg.replyTo);
+                switch (msg.what) {
+                    case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION:
+                        if (channel != null) {
+                            Log.d(TAG, "duplicate client connection: " + msg.sendingUid);
+                            channel.replyToMessage(msg,
+                                    AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+                                    AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED);
+                        } else {
+                            channel = new AsyncChannel();
+                            mClients.put(msg.replyTo, channel);
+                            channel.connected(context, this, msg.replyTo);
+                            channel.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+                                    AsyncChannel.STATUS_SUCCESSFUL);
+                        }
+                        break;
+                    case AsyncChannel.CMD_CHANNEL_DISCONNECT:
+                        channel.disconnect();
+                        break;
+
+                    case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+                        mClients.remove(msg.replyTo);
+                        break;
+
+                    default:
+                        messageHandler.handleMessage(msg);
+                        break;
+                }
+            }
+        };
+        mMessenger = new Messenger(handler);
+    }
+
+    public Messenger getMessenger() {
+        return mMessenger;
+    }
+
+}