Implement VVM Task Scheduling

Before CL multiple activate events will be fired during boot and each
one of them will be processed, which is redundant. The activation will
also trigger multiple sync events. During the initial download of
multiple voicemails an upload will also be trigger for each voicemail.
The flood of connections is know to have the client banned by the
server. Codes exists for retrying, but does not actually do anything.

In this CL TaskSchedulerService is implemented which will prevent
duplicated tasks from being queued, throttle requests, and handle
retries.

"Activate" event is not in scheduling yet.

- OmtpVvmSyncService is no longer a service. It will be renamed later.
  It can now be called to sync voicemail in a single threaded manner.

Fixes: 28729940
Bug: 28730056

Change-Id: I3678d8a16326e9a181bb401c003574928f02ae00
diff --git a/tests/Android.mk b/tests/Android.mk
index e1b564f..59cba42 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -25,7 +25,7 @@
 
 LOCAL_MODULE_TAGS := tests
 
-LOCAL_JAVA_LIBRARIES := telephony-common
+LOCAL_JAVA_LIBRARIES := telephony-common android-support-test
 
 LOCAL_INSTRUMENTATION_FOR := TeleService
 
diff --git a/tests/src/com/android/phone/vvm/omtp/scheduling/BaseTaskTest.java b/tests/src/com/android/phone/vvm/omtp/scheduling/BaseTaskTest.java
new file mode 100644
index 0000000..27dd87e
--- /dev/null
+++ b/tests/src/com/android/phone/vvm/omtp/scheduling/BaseTaskTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.phone.vvm.omtp.scheduling;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.phone.vvm.omtp.scheduling.Task.TaskId;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class BaseTaskTest extends BaseTaskTestBase {
+
+
+    @Test
+    public void testBaseTask() {
+        DummyBaseTask task = (DummyBaseTask) submitTask(
+                BaseTask.createIntent(mTestContext, DummyBaseTask.class, 123));
+        assertTrue(task.getId().equals(new TaskId(1, 123)));
+        assertTrue(!task.hasStarted());
+        assertTrue(!task.hasRun);
+        mService.runNextTask();
+        assertTrue(task.hasStarted());
+        assertTrue(task.hasRun);
+        verify(task.policy).onBeforeExecute();
+        verify(task.policy).onCompleted();
+    }
+
+    @Test
+    public void testFail() {
+        FailingBaseTask task = (FailingBaseTask) submitTask(
+                BaseTask.createIntent(mTestContext, FailingBaseTask.class, 0));
+        mService.runNextTask();
+        verify(task.policy).onFail();
+    }
+
+    @Test
+    public void testDuplicated() {
+        DummyBaseTask task1 = (DummyBaseTask) submitTask(
+                BaseTask.createIntent(mTestContext, DummyBaseTask.class, 123));
+        verify(task1.policy, never()).onDuplicatedTaskAdded();
+
+        DummyBaseTask task2 = (DummyBaseTask) submitTask(
+                BaseTask.createIntent(mTestContext, DummyBaseTask.class, 123));
+        verify(task1.policy).onDuplicatedTaskAdded();
+
+        mService.runNextTask();
+        assertTrue(task1.hasRun);
+        assertTrue(!task2.hasRun);
+    }
+
+    @Test
+    public void testDuplicated_DifferentSubId() {
+        DummyBaseTask task1 = (DummyBaseTask) submitTask(
+                BaseTask.createIntent(mTestContext, DummyBaseTask.class, 123));
+        verify(task1.policy, never()).onDuplicatedTaskAdded();
+
+        DummyBaseTask task2 = (DummyBaseTask) submitTask(
+                BaseTask.createIntent(mTestContext, DummyBaseTask.class, 456));
+        verify(task1.policy, never()).onDuplicatedTaskAdded();
+        mService.runNextTask();
+        assertTrue(task1.hasRun);
+        assertTrue(!task2.hasRun);
+
+        mService.runNextTask();
+        assertTrue(task2.hasRun);
+    }
+
+    @Test
+    public void testReadyTime() {
+        BaseTask task = spy(new DummyBaseTask());
+        assertTrue(task.getReadyInMilliSeconds() == 0);
+        mTime = 500;
+        assertTrue(task.getReadyInMilliSeconds() == -500);
+        task.setExecutionTime(1000);
+        assertTrue(task.getReadyInMilliSeconds() == 500);
+    }
+
+    public static class DummyBaseTask extends BaseTask {
+
+        public Policy policy;
+        public boolean hasRun = false;
+
+        public DummyBaseTask() {
+            super(1);
+            policy = mock(Policy.class);
+            addPolicy(policy);
+        }
+
+        @Override
+        public void onExecuteInBackgroundThread() {
+            hasRun = true;
+        }
+    }
+
+    public static class FailingBaseTask extends BaseTask {
+
+        public Policy policy;
+        public FailingBaseTask() {
+            super(1);
+            policy = mock(Policy.class);
+            addPolicy(policy);
+        }
+
+        @Override
+        public void onExecuteInBackgroundThread() {
+            fail();
+        }
+    }
+}
diff --git a/tests/src/com/android/phone/vvm/omtp/scheduling/BaseTaskTestBase.java b/tests/src/com/android/phone/vvm/omtp/scheduling/BaseTaskTestBase.java
new file mode 100644
index 0000000..1ffd3c4
--- /dev/null
+++ b/tests/src/com/android/phone/vvm/omtp/scheduling/BaseTaskTestBase.java
@@ -0,0 +1,50 @@
+/*
+ * 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.phone.vvm.omtp.scheduling;
+
+import com.android.phone.vvm.omtp.scheduling.BaseTask.Clock;
+
+import org.junit.After;
+import org.junit.Before;
+
+public class BaseTaskTestBase extends TaskSchedulerServiceTestBase {
+
+    /**
+     * "current time" of the deterministic clock.
+     */
+    public long mTime;
+
+    @Before
+    public void setUpBaseTaskTest() {
+        mTime = 0;
+        BaseTask.setClockForTesting(new TestClock());
+    }
+
+    @After
+    public void tearDownBaseTaskTest() {
+        BaseTask.setClockForTesting(new Clock());
+    }
+
+
+    private class TestClock extends Clock {
+
+        @Override
+        public long getTimeMillis() {
+            return mTime;
+        }
+    }
+}
diff --git a/tests/src/com/android/phone/vvm/omtp/scheduling/PolicyTest.java b/tests/src/com/android/phone/vvm/omtp/scheduling/PolicyTest.java
new file mode 100644
index 0000000..9761d01
--- /dev/null
+++ b/tests/src/com/android/phone/vvm/omtp/scheduling/PolicyTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.phone.vvm.omtp.scheduling;
+
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PolicyTest extends BaseTaskTestBase {
+
+    private static int sExecuteCounter;
+
+    @Before
+    public void setUpPolicyTest() {
+        sExecuteCounter = 0;
+    }
+
+    @Test
+    public void testPostponePolicy() {
+        Task task = submitTask(BaseTask.createIntent(mTestContext, PostponeTask.class, 0));
+        mService.runNextTask();
+        assertTrue(task.getReadyInMilliSeconds() == 1000);
+        submitTask(BaseTask.createIntent(mTestContext, PostponeTask.class, 0));
+        assertTrue(task.getReadyInMilliSeconds() == 1000);
+        mTime = 500;
+        submitTask(BaseTask.createIntent(mTestContext, PostponeTask.class, 0));
+        assertTrue(task.getReadyInMilliSeconds() == 1000);
+        mTime = 2500;
+        mService.runNextTask();
+        assertTrue(sExecuteCounter == 1);
+    }
+
+    @Test
+    public void testRetryPolicy() {
+        Task task = submitTask(BaseTask.createIntent(mTestContext, FailingRetryTask.class, 0));
+        mService.runNextTask();
+        // Should queue retry at 1000
+        assertTrue(sExecuteCounter == 1);
+        mService.runNextTask();
+        assertTrue(sExecuteCounter == 1);
+        mTime = 1500;
+        mService.runNextTask();
+        // Should queue retry at 2500
+        assertTrue(sExecuteCounter == 2);
+        mService.runNextTask();
+        assertTrue(sExecuteCounter == 2);
+        mTime = 2000;
+        mService.runNextTask();
+        assertTrue(sExecuteCounter == 2);
+        mTime = 3000;
+        mService.runNextTask();
+        // No more retries are queued.
+        assertTrue(sExecuteCounter == 3);
+        mService.runNextTask();
+        assertTrue(sExecuteCounter == 3);
+        mTime = 4500;
+        mService.runNextTask();
+        assertTrue(sExecuteCounter == 3);
+    }
+
+    @Test
+    public void testMinimalIntervalPolicy() {
+        MinimalIntervalPolicyTask task1 = (MinimalIntervalPolicyTask) submitTask(
+                BaseTask.createIntent(mTestContext, MinimalIntervalPolicyTask.class, 0));
+        mService.runNextTask();
+        assertTrue(task1.hasRan);
+        MinimalIntervalPolicyTask task2 = (MinimalIntervalPolicyTask) submitTask(
+                BaseTask.createIntent(mTestContext, MinimalIntervalPolicyTask.class, 0));
+        mService.runNextTask();
+        assertTrue(!task2.hasRan);
+
+        mTime = 1500;
+        mService.runNextTask();
+
+        MinimalIntervalPolicyTask task3 = (MinimalIntervalPolicyTask) submitTask(
+                BaseTask.createIntent(mTestContext, MinimalIntervalPolicyTask.class, 0));
+        mService.runNextTask();
+        assertTrue(task3.hasRan);
+    }
+
+    public abstract static class PolicyTestTask extends BaseTask {
+
+        public PolicyTestTask() {
+            super(1);
+        }
+
+        @Override
+        public void onExecuteInBackgroundThread() {
+            sExecuteCounter++;
+        }
+    }
+
+    public static class PostponeTask extends PolicyTestTask {
+
+        PostponeTask() {
+            addPolicy(new PostponePolicy(1000));
+        }
+    }
+
+    public static class FailingRetryTask extends PolicyTestTask {
+
+        public FailingRetryTask() {
+            addPolicy(new RetryPolicy(2, 1000));
+        }
+
+        @Override
+        public void onExecuteInBackgroundThread() {
+            super.onExecuteInBackgroundThread();
+            fail();
+        }
+    }
+
+    public static class MinimalIntervalPolicyTask extends PolicyTestTask {
+
+        boolean hasRan;
+
+        MinimalIntervalPolicyTask() {
+            addPolicy(new MinimalIntervalPolicy(1000));
+        }
+
+        @Override
+        public void onExecuteInBackgroundThread() {
+            super.onExecuteInBackgroundThread();
+            hasRan = true;
+        }
+    }
+
+}
diff --git a/tests/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerServiceTest.java b/tests/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerServiceTest.java
new file mode 100644
index 0000000..2dd4ecf
--- /dev/null
+++ b/tests/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerServiceTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.phone.vvm.omtp.scheduling;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.phone.vvm.omtp.scheduling.Task.TaskId;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+public class TaskSchedulerServiceTest extends TaskSchedulerServiceTestBase {
+
+    @Test
+    public void testTaskIdComparison() {
+        TaskId id1 = new TaskId(1, 1);
+        TaskId id2 = new TaskId(1, 1);
+        TaskId id3 = new TaskId(1, 2);
+        assertTrue(id1.equals(id2));
+        assertTrue(id1.equals(id1));
+        assertTrue(!id1.equals(id3));
+    }
+
+    @Test
+    public void testAddDuplicatedTask() throws TimeoutException {
+        TestTask task1 = (TestTask) submitTask(
+                TaskSchedulerService.createIntent(mTestContext, TestTask.class));
+        TestTask task2 = (TestTask) submitTask(
+                TaskSchedulerService.createIntent(mTestContext, TestTask.class));
+        assertTrue(task1.onDuplicatedTaskAddedCounter.invokedOnce());
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        verifyNotRan(task2);
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        verifyNotRan(task2);
+    }
+
+    @Test
+    public void testAddDuplicatedTaskAfterFirstCompleted() throws TimeoutException {
+        TestTask task1 = (TestTask) submitTask(
+                TaskSchedulerService.createIntent(mTestContext, TestTask.class));
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        TestTask task2 = (TestTask) submitTask(
+                TaskSchedulerService.createIntent(mTestContext, TestTask.class));
+        assertTrue(task1.onDuplicatedTaskAddedCounter.neverInvoked());
+        mService.runNextTask();
+        verifyRanOnce(task2);
+    }
+
+    @Test
+    public void testAddMultipleTask() {
+        TestTask task1 = (TestTask) submitTask(
+                putTaskId(TaskSchedulerService.createIntent(mTestContext, TestTask.class),
+                        new TaskId(1, 0)));
+        TestTask task2 = (TestTask) submitTask(
+                putTaskId(TaskSchedulerService.createIntent(mTestContext, TestTask.class),
+                        new TaskId(2, 0)));
+        TestTask task3 = (TestTask) submitTask(
+                putTaskId(TaskSchedulerService.createIntent(mTestContext, TestTask.class),
+                        new TaskId(1, 1)));
+        assertTrue(task1.onDuplicatedTaskAddedCounter.neverInvoked());
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        verifyNotRan(task2);
+        verifyNotRan(task3);
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        verifyRanOnce(task2);
+        verifyNotRan(task3);
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        verifyRanOnce(task2);
+        verifyRanOnce(task3);
+    }
+
+    @Test
+    public void testNotReady() {
+        TestTask task1 = (TestTask) submitTask(
+                putTaskId(TaskSchedulerService.createIntent(mTestContext, TestTask.class),
+                        new TaskId(1, 0)));
+        task1.readyInMilliseconds = 1000;
+        mService.runNextTask();
+        verifyNotRan(task1);
+        TestTask task2 = (TestTask) submitTask(
+                putTaskId(TaskSchedulerService.createIntent(mTestContext, TestTask.class),
+                        new TaskId(2, 0)));
+        mService.runNextTask();
+        verifyNotRan(task1);
+        verifyRanOnce(task2);
+        task1.readyInMilliseconds = 50;
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        verifyRanOnce(task2);
+    }
+
+    @Test
+    public void testInvalidTaskId() {
+        Task task = mock(Task.class);
+        when(task.getId()).thenReturn(new TaskId(Task.TASK_INVALID, 0));
+        thrown.expect(AssertionError.class);
+        mService.addTask(task);
+    }
+
+    @Test
+    public void testDuplicatesAllowedTaskId() {
+        TestTask task1 = (TestTask) submitTask(
+                putTaskId(TaskSchedulerService.createIntent(mTestContext, TestTask.class),
+                        new TaskId(Task.TASK_ALLOW_DUPLICATES, 0)));
+        TestTask task2 = (TestTask) submitTask(
+                putTaskId(TaskSchedulerService.createIntent(mTestContext, TestTask.class),
+                        new TaskId(Task.TASK_ALLOW_DUPLICATES, 0)));
+        assertTrue(task1.onDuplicatedTaskAddedCounter.neverInvoked());
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        verifyNotRan(task2);
+        mService.runNextTask();
+        verifyRanOnce(task1);
+        verifyRanOnce(task2);
+    }
+}
diff --git a/tests/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerServiceTestBase.java b/tests/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerServiceTestBase.java
new file mode 100644
index 0000000..63f5c2f
--- /dev/null
+++ b/tests/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerServiceTestBase.java
@@ -0,0 +1,230 @@
+/*
+ * 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.phone.vvm.omtp.scheduling;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Message;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ServiceTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.phone.Assert;
+import com.android.phone.vvm.omtp.scheduling.Task.TaskId;
+import com.android.phone.vvm.omtp.scheduling.TaskSchedulerService.MainThreadHandler;
+import com.android.phone.vvm.omtp.scheduling.TaskSchedulerService.WorkerThreadHandler;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+public class TaskSchedulerServiceTestBase {
+
+    private static final String EXTRA_ID = "test_extra_id";
+    private static final String EXTRA_SUB_ID = "test_extra_sub_id";
+
+    public TaskSchedulerService mService;
+
+    @Rule
+    public final ExpectedException thrown = ExpectedException.none();
+
+    @Rule
+    public final ServiceTestRule mServiceRule = new ServiceTestRule();
+
+    public Context mTargetContext;
+    public Context mTestContext;
+
+    private static boolean sIsMainThread = true;
+
+    private final TestMessageSender mMessageSender = new TestMessageSender();
+
+    public static Intent putTaskId(Intent intent, TaskId taskId) {
+        intent.putExtra(EXTRA_ID, taskId.id);
+        intent.putExtra(EXTRA_SUB_ID, taskId.subId);
+        return intent;
+    }
+
+    public static TaskId getTaskId(Intent intent) {
+        return new TaskId(intent.getIntExtra(EXTRA_ID, 0), intent.getIntExtra(EXTRA_SUB_ID, 0));
+    }
+
+    @Before
+    public void setUp() throws TimeoutException {
+        Assert.setIsMainThreadForTesting(true);
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        IBinder binder = null;
+        // bindService() might returns null on 2nd invocation because the service is not unbinded
+        // yet. See https://code.google.com/p/android/issues/detail?id=180396
+        while (binder == null) {
+            binder = mServiceRule
+                    .bindService(new Intent(mTargetContext, TaskSchedulerService.class));
+        }
+        mService = ((TaskSchedulerService.LocalBinder) binder).getService();
+        mTestContext = createTestContext(mTargetContext, mService);
+        mService.setMessageSenderForTest(mMessageSender);
+        mService.setTaskAutoRunDisabledForTest(true);
+        mService.setContextForTest(mTestContext);
+    }
+
+    @After
+    public void tearDown() {
+        Assert.setIsMainThreadForTesting(null);
+        mService.setTaskAutoRunDisabledForTest(false);
+        mService.clearTasksForTest();
+        mService.stopSelf();
+    }
+
+    public Task submitTask(Intent intent) {
+        Task task = mService.createTask(intent, 0, 0);
+        mService.addTask(task);
+        return task;
+    }
+
+    public static void verifyRanOnce(TestTask task) {
+        assertTrue(task.onBeforeExecuteCounter.invokedOnce());
+        assertTrue(task.executeCounter.invokedOnce());
+        assertTrue(task.onCompletedCounter.invokedOnce());
+    }
+
+    public static void verifyNotRan(TestTask task) {
+        assertTrue(task.onBeforeExecuteCounter.neverInvoked());
+        assertTrue(task.executeCounter.neverInvoked());
+        assertTrue(task.onCompletedCounter.neverInvoked());
+    }
+
+    public static class TestTask implements Task {
+
+        public int readyInMilliseconds;
+
+        private TaskId mId;
+
+        public final InvocationCounter onCreateCounter = new InvocationCounter();
+        public final InvocationCounter onBeforeExecuteCounter = new InvocationCounter();
+        public final InvocationCounter executeCounter = new InvocationCounter();
+        public final InvocationCounter onCompletedCounter = new InvocationCounter();
+        public final InvocationCounter onDuplicatedTaskAddedCounter = new InvocationCounter();
+
+        @Override
+        public void onCreate(Context context, Intent intent, int flags, int startId) {
+            onCreateCounter.invoke();
+            mId = getTaskId(intent);
+        }
+
+        @Override
+        public TaskId getId() {
+            return mId;
+        }
+
+        @Override
+        public long getReadyInMilliSeconds() {
+            Assert.isMainThread();
+            return readyInMilliseconds;
+        }
+
+        @Override
+        public void onBeforeExecute() {
+            Assert.isMainThread();
+            onBeforeExecuteCounter.invoke();
+        }
+
+        @Override
+        public void onExecuteInBackgroundThread() {
+            Assert.isNotMainThread();
+            executeCounter.invoke();
+        }
+
+        @Override
+        public void onCompleted() {
+            Assert.isMainThread();
+            onCompletedCounter.invoke();
+        }
+
+        @Override
+        public void onDuplicatedTaskAdded(Task task) {
+            Assert.isMainThread();
+            onDuplicatedTaskAddedCounter.invoke();
+        }
+    }
+
+    public static class InvocationCounter {
+
+        private int mCounter;
+
+        public void invoke() {
+            mCounter++;
+        }
+
+        public boolean invokedOnce() {
+            return mCounter == 1;
+        }
+
+        public boolean neverInvoked() {
+            return mCounter == 0;
+        }
+    }
+
+    private class TestMessageSender extends TaskSchedulerService.MessageSender {
+
+        @Override
+        public void send(Message message) {
+            if (message.getTarget() instanceof MainThreadHandler) {
+                Assert.setIsMainThreadForTesting(true);
+            } else if (message.getTarget() instanceof WorkerThreadHandler) {
+                Assert.setIsMainThreadForTesting(false);
+            } else {
+                throw new AssertionError("unexpected Handler " + message.getTarget());
+            }
+            message.getTarget().handleMessage(message);
+        }
+    }
+
+    public static void assertTrue(boolean condition) {
+        if (!condition) {
+            throw new AssertionError();
+        }
+    }
+
+    private static Context createTestContext(Context targetContext, TaskSchedulerService service) {
+        TestContext context = mock(TestContext.class);
+        when(context.getService()).thenReturn(service);
+        when(context.startService(any())).thenCallRealMethod();
+        when(context.getPackageName()).thenReturn(targetContext.getPackageName());
+        return context;
+    }
+
+    public abstract class TestContext extends Context {
+
+        @Override
+        public ComponentName startService(Intent service) {
+            getService().onStartCommand(service, 0, 0);
+            return null;
+        }
+
+        public abstract TaskSchedulerService getService();
+    }
+}