--- /dev/null
+#
+# 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)
--- /dev/null
+/*
+ * 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);
+ }
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.");
+ }
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}