diff --git a/core/java/com/android/internal/util/WakeupMessage.java b/core/java/com/android/internal/util/WakeupMessage.java index 77859b8bb6a01..451078b1b0476 100644 --- a/core/java/com/android/internal/util/WakeupMessage.java +++ b/core/java/com/android/internal/util/WakeupMessage.java @@ -21,7 +21,7 @@ import android.content.Context; import android.os.Handler; import android.os.Message; - /** +/** * An AlarmListener that sends the specified message to a Handler and keeps the system awake until * the message is processed. * @@ -33,19 +33,17 @@ import android.os.Message; * the message, but does not guarantee that the system will be awake until the target object has * processed it. This is because as soon as the onAlarmListener sends the message and returns, the * AlarmManager releases its wakelock and the system is free to go to sleep again. - * */ public class WakeupMessage implements AlarmManager.OnAlarmListener { - private static AlarmManager sAlarmManager; + private final AlarmManager mAlarmManager; private final Handler mHandler; private final String mCmdName; private final int mCmd, mArg1, mArg2; + private boolean mScheduled; public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1, int arg2) { - if (sAlarmManager == null) { - sAlarmManager = context.getSystemService(AlarmManager.class); - } + mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mHandler = handler; mCmdName = cmdName; mCmd = cmd; @@ -61,19 +59,43 @@ public class WakeupMessage implements AlarmManager.OnAlarmListener { this(context, handler, cmdName, cmd, 0, 0); } - public void schedule(long when) { - sAlarmManager.setExact( + /** + * Schedule the message to be delivered at the time in milliseconds of the + * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup + * the device when it goes off. If schedule is called multiple times without the message being + * dispatched then the alarm is rescheduled to the new time. + */ + public synchronized void schedule(long when) { + mAlarmManager.setExact( AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler); + mScheduled = true; } - public void cancel() { - sAlarmManager.cancel(this); + /** + * Cancel all pending messages. This includes alarms that may have been fired, but have not been + * run on the handler yet. + */ + public synchronized void cancel() { + if (mScheduled) { + mAlarmManager.cancel(this); + mScheduled = false; + } } @Override public void onAlarm() { - Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2); - mHandler.handleMessage(msg); - msg.recycle(); + // Once this method is called the alarm has already been fired and removed from + // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now + // be marked as unscheduled so that it can be rescheduled in the message handler. + final boolean stillScheduled; + synchronized (this) { + stillScheduled = mScheduled; + mScheduled = false; + } + if (stillScheduled) { + Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2); + mHandler.handleMessage(msg); + msg.recycle(); + } } } diff --git a/core/tests/utiltests/Android.mk b/core/tests/utiltests/Android.mk index f949e1ab02ab2..3c6c32ec0d275 100644 --- a/core/tests/utiltests/Android.mk +++ b/core/tests/utiltests/Android.mk @@ -12,7 +12,8 @@ LOCAL_MODULE_TAGS := tests LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-test + android-support-test \ + mockito-target LOCAL_JAVA_LIBRARIES := android.test.runner diff --git a/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java b/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java new file mode 100644 index 0000000000000..da8bc1d0945d9 --- /dev/null +++ b/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java @@ -0,0 +1,169 @@ +/* + * 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; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import android.app.AlarmManager; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.test.suitebuilder.annotation.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit tests for {@link com.android.internal.util.WakeupMessage}. + */ +@SmallTest +public class WakeupMessageTest { + private static final String TEST_CMD_NAME = "TEST cmd Name"; + private static final int TEST_CMD = 18; + private static final int TEST_ARG1 = 33; + private static final int TEST_ARG2 = 182; + + @Mock AlarmManager mAlarmManager; + WakeupMessage mMessage; + // Make a spy so that we can verify calls to it + @Spy MessageCapturingHandler mHandler = new MessageCapturingHandler(); + + ArgumentCaptor mListenerCaptor = + ArgumentCaptor.forClass(AlarmManager.OnAlarmListener.class); + + /** + * A Handler that will capture the most recent message sent to it. + * + * This handler is setup on the main Looper + */ + public static class MessageCapturingHandler extends Handler { + private Message mLastMessage; + + public MessageCapturingHandler() { + super(Looper.getMainLooper(), /* Nothing is actually dispatched on this Looper */ + null, false); + } + + @Override + public void handleMessage(Message m) { + // need to copy since it will be recycled after this method returns + mLastMessage = Message.obtain(m); + } + + public Message getLastMessage() { + return mLastMessage; + } + } + + /** + * Sets up the test. + */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Context context = mock(Context.class); + when(context.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager); + // capture the listener for each AlarmManager.setExact call + doNothing().when(mAlarmManager).setExact(anyInt(), anyLong(), any(String.class), + mListenerCaptor.capture(), any(Handler.class)); + + mMessage = new WakeupMessage(context, mHandler, TEST_CMD_NAME, TEST_CMD, TEST_ARG1, + TEST_ARG2); + } + + /** + * Ensure the test is cleaned up and ready for the next test. + */ + @After + public void cleanup() { + validateMockitoUsage(); + } + + private void scheduleAndVerifyAlarm(long when) { + mMessage.schedule(when); + verify(mAlarmManager).setExact(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), eq(when), + eq(TEST_CMD_NAME), any(AlarmManager.OnAlarmListener.class), eq(mHandler)); + } + + private void verifyMessageDispatchedOnce() { + verify(mHandler, times(1)).handleMessage(any(Message.class)); + assertEquals("what", TEST_CMD, mHandler.getLastMessage().what); + assertEquals("arg1", TEST_ARG1, mHandler.getLastMessage().arg1); + assertEquals("arg2", TEST_ARG2, mHandler.getLastMessage().arg2); + } + + /** + * Schedule and deliver a single message + */ + @Test + public void scheduleAndDeliverMessage() { + final long when = 1001; + scheduleAndVerifyAlarm(when); + verify(mHandler, never()).handleMessage(any(Message.class)); + mListenerCaptor.getValue().onAlarm(); + verifyMessageDispatchedOnce(); + } + + /** + * Check that the message is not delivered if cancel is called it after its alarm fires but + * before onAlarm is called. + * + * This ensures that if cancel is called on the handler thread, any previously-scheduled message + * is guaranteed not to be delivered. + */ + @Test + public void scheduleAndCancelMessage() { + final long when = 1010; + scheduleAndVerifyAlarm(when); + mMessage.cancel(); + mListenerCaptor.getValue().onAlarm(); + verify(mHandler, never()).handleMessage(any(Message.class)); + } + + /** + * Verify nothing happens when cancel is called without a schedule + */ + @Test + public void cancelWithoutSchedule() { + mMessage.cancel(); + } + + /** + * Verify that the message is silently rescheduled if schedule is called twice without the + * message being dispatched first. + */ + @Test + public void scheduleTwiceWithoutMessageDispatched() { + final long when1 = 1011; + final long when2 = 1012; + scheduleAndVerifyAlarm(when1); + scheduleAndVerifyAlarm(when2); + mListenerCaptor.getValue().onAlarm(); + verifyMessageDispatchedOnce(); + } + +}