blob: cfe2cef7db519bb0568b05eb8f48bd65339f8491 [file] [log] [blame]
/*
* Copyright 2018 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 androidx.work.impl.background.systemalarm;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
import androidx.work.Configuration;
import androidx.work.Constraints;
import androidx.work.DatabaseTest;
import androidx.work.OneTimeWorkRequest;
import androidx.work.State;
import androidx.work.impl.Processor;
import androidx.work.impl.Scheduler;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.constraints.trackers.BatteryChargingTracker;
import androidx.work.impl.constraints.trackers.BatteryNotLowTracker;
import androidx.work.impl.constraints.trackers.NetworkStateTracker;
import androidx.work.impl.constraints.trackers.StorageNotLowTracker;
import androidx.work.impl.constraints.trackers.Trackers;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkSpecDao;
import androidx.work.worker.SleepTestWorker;
import androidx.work.worker.TestWorker;
import org.hamcrest.collection.IsIterableContainingInOrder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SystemAlarmDispatcherTest extends DatabaseTest {
private static final int START_ID = 0;
// Test timeout in seconds - this needs to be longer than SleepTestWorker.SLEEP_DURATION
private static final int TEST_TIMEOUT = 6;
private Context mContext;
private Scheduler mScheduler;
private WorkManagerImpl mWorkManager;
private Configuration mConfiguration;
private ExecutorService mExecutorService;
private Processor mProcessor;
private Processor mSpyProcessor;
private CommandInterceptingSystemDispatcher mDispatcher;
private CommandInterceptingSystemDispatcher mSpyDispatcher;
private SystemAlarmDispatcher.CommandsCompletedListener mCompletedListener;
private CountDownLatch mLatch;
private Trackers mTracker;
private BatteryChargingTracker mBatteryChargingTracker;
private BatteryNotLowTracker mBatteryNotLowTracker;
private NetworkStateTracker mNetworkStateTracker;
private StorageNotLowTracker mStorageNotLowTracker;
@Before
public void setUp() {
mContext = InstrumentationRegistry.getTargetContext().getApplicationContext();
mScheduler = mock(Scheduler.class);
mWorkManager = mock(WorkManagerImpl.class);
mLatch = new CountDownLatch(1);
mCompletedListener = new SystemAlarmDispatcher.CommandsCompletedListener() {
@Override
public void onAllCommandsCompleted() {
mLatch.countDown();
}
};
mConfiguration = new Configuration.Builder().build();
when(mWorkManager.getWorkDatabase()).thenReturn(mDatabase);
when(mWorkManager.getConfiguration()).thenReturn(mConfiguration);
mExecutorService = Executors.newSingleThreadExecutor();
mProcessor = new Processor(
mContext,
mConfiguration,
mDatabase,
Collections.singletonList(mScheduler),
// simulate real world use-case
mExecutorService);
mSpyProcessor = spy(mProcessor);
mDispatcher =
new CommandInterceptingSystemDispatcher(mContext, mSpyProcessor, mWorkManager);
mDispatcher.setCompletedListener(mCompletedListener);
mSpyDispatcher = spy(mDispatcher);
mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext));
mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext));
// Requires API 24+ types.
mNetworkStateTracker = mock(NetworkStateTracker.class);
mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext));
mTracker = mock(Trackers.class);
when(mTracker.getBatteryChargingTracker()).thenReturn(mBatteryChargingTracker);
when(mTracker.getBatteryNotLowTracker()).thenReturn(mBatteryNotLowTracker);
when(mTracker.getNetworkStateTracker()).thenReturn(mNetworkStateTracker);
when(mTracker.getStorageNotLowTracker()).thenReturn(mStorageNotLowTracker);
// Override Trackers being used by WorkConstraintsProxy
Trackers.setInstance(mTracker);
}
@After
public void tearDown() {
mExecutorService.shutdownNow();
try {
mExecutorService.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
// Do nothing.
}
mSpyDispatcher.onDestroy();
}
@Test
public void testSchedule() throws InterruptedException {
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInitialDelay(TimeUnit.HOURS.toMillis(1), TimeUnit.MILLISECONDS).build();
insertWork(work);
String workSpecId = work.getStringId();
final Intent intent = CommandHandler.createScheduleWorkIntent(mContext, workSpecId);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, intent, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
}
@Test
public void testDelayMet_success() throws InterruptedException {
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.build();
insertWork(work);
String workSpecId = work.getStringId();
final Intent intent = CommandHandler.createDelayMetIntent(mContext, workSpecId);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, intent, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
assertThat(mLatch.getCount(), is(0L));
verify(mSpyProcessor, times(1)).startWork(workSpecId);
}
@Test
public void testDelayMet_withStop() throws InterruptedException {
// SleepTestWorker sleeps for 5 seconds
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(SleepTestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInitialDelay(TimeUnit.HOURS.toMillis(1), TimeUnit.MILLISECONDS)
.build();
insertWork(work);
String workSpecId = work.getStringId();
final Intent delayMet = CommandHandler.createDelayMetIntent(mContext, workSpecId);
final Intent stopWork = CommandHandler.createStopWorkIntent(mContext, workSpecId);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, delayMet, START_ID));
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, stopWork, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
assertThat(mLatch.getCount(), is(0L));
verify(mSpyProcessor, times(1)).startWork(workSpecId);
verify(mWorkManager, times(1)).stopWork(workSpecId);
}
@Test
public void testDelayMet_withStopWhenCancelled() throws InterruptedException {
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(SleepTestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.build();
insertWork(work);
String workSpecId = work.getStringId();
final Intent scheduleWork = CommandHandler.createDelayMetIntent(mContext, workSpecId);
final Intent stopWork = CommandHandler.createStopWorkIntent(mContext, workSpecId);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, scheduleWork, START_ID));
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, stopWork, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
assertThat(mLatch.getCount(), is(0L));
verify(mSpyProcessor, times(1)).startWork(workSpecId);
verify(mWorkManager, times(1)).stopWork(workSpecId);
}
@Test
public void testSchedule_withConstraints() throws InterruptedException {
when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(
System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1),
TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
.setRequiresCharging(true)
.build())
.build();
insertWork(work);
String workSpecId = work.getStringId();
final Intent scheduleWork = CommandHandler.createScheduleWorkIntent(mContext, workSpecId);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, scheduleWork, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
assertThat(mLatch.getCount(), is(0L));
// Should not call startWork, but schedule an alarm.
verify(mSpyProcessor, times(0)).startWork(workSpecId);
}
@Test
public void testConstraintsChanged_withNoConstraints() throws InterruptedException {
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setScheduleRequestedAt(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.build();
insertWork(work);
final Intent constraintChanged = CommandHandler.createConstraintsChangedIntent(mContext);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, constraintChanged, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
assertThat(mLatch.getCount(), is(0L));
}
@Test
public void testConstraintsChangedMarkedNotScheduled_withNoConstraints()
throws InterruptedException {
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setScheduleRequestedAt(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.build();
insertWork(work);
String workSpecId = work.getStringId();
final Intent constraintChanged = CommandHandler.createConstraintsChangedIntent(mContext);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, constraintChanged, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
assertThat(mLatch.getCount(), is(0L));
verify(mSpyProcessor, times(0)).startWork(workSpecId);
}
@Test
public void testConstraintsChanged_withConstraint() throws InterruptedException {
when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
.setRequiresCharging(true)
.build())
.build();
insertWork(work);
final Intent constraintChanged = CommandHandler.createConstraintsChangedIntent(mContext);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, constraintChanged, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
assertThat(mLatch.getCount(), is(0L));
}
@Test
@LargeTest
public void testDelayMet_withUnMetConstraint() throws InterruptedException {
when(mBatteryChargingTracker.getInitialState()).thenReturn(false);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
.setRequiresCharging(true)
.build())
.build();
insertWork(work);
Intent delayMet = CommandHandler.createDelayMetIntent(mContext, work.getStringId());
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, delayMet, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
List<String> intentActions = intentActionsFor(mSpyDispatcher.getCommands());
WorkSpecDao workSpecDao = mDatabase.workSpecDao();
WorkSpec workSpec = workSpecDao.getWorkSpec(work.getStringId());
assertThat(mLatch.getCount(), is(0L));
// Verify order of events
assertThat(intentActions,
IsIterableContainingInOrder.contains(
CommandHandler.ACTION_DELAY_MET,
CommandHandler.ACTION_STOP_WORK,
CommandHandler.ACTION_EXECUTION_COMPLETED,
CommandHandler.ACTION_CONSTRAINTS_CHANGED));
assertThat(workSpec.state, is(State.ENQUEUED));
}
@Test
public void testDelayMet_withMetConstraint() throws InterruptedException {
when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
.setRequiresCharging(true)
.build())
.build();
insertWork(work);
Intent delayMet = CommandHandler.createDelayMetIntent(mContext, work.getStringId());
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, delayMet, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
List<String> intentActions = intentActionsFor(mSpyDispatcher.getCommands());
WorkSpecDao workSpecDao = mDatabase.workSpecDao();
WorkSpec workSpec = workSpecDao.getWorkSpec(work.getStringId());
assertThat(mLatch.getCount(), is(0L));
// Assert order of events
assertThat(intentActions,
IsIterableContainingInOrder.contains(
CommandHandler.ACTION_DELAY_MET,
CommandHandler.ACTION_EXECUTION_COMPLETED,
CommandHandler.ACTION_CONSTRAINTS_CHANGED));
assertThat(workSpec.state, is(State.SUCCEEDED));
}
@Test
public void testReschedule() throws InterruptedException {
// Use a mocked scheduler in this test.
Scheduler scheduler = mock(Scheduler.class);
doCallRealMethod().when(mWorkManager).rescheduleEligibleWork();
when(mWorkManager.getSchedulers()).thenReturn(Collections.singletonList(scheduler));
OneTimeWorkRequest failed = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInitialState(State.FAILED)
.build();
OneTimeWorkRequest succeeded = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInitialState(State.SUCCEEDED)
.build();
OneTimeWorkRequest noConstraints = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.build();
OneTimeWorkRequest workWithConstraints = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
.setRequiresCharging(true)
.build())
.build();
insertWork(failed);
insertWork(succeeded);
insertWork(noConstraints);
insertWork(workWithConstraints);
Intent reschedule = CommandHandler.createRescheduleIntent(mContext);
mSpyDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mSpyDispatcher, reschedule, START_ID));
mLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
assertThat(mLatch.getCount(), is(0L));
ArgumentCaptor<WorkSpec> captor = ArgumentCaptor.forClass(WorkSpec.class);
verify(scheduler, times(1))
.schedule(captor.capture());
Set<String> capturedIds = new HashSet<>();
List<WorkSpec> workSpecs = captor.getAllValues();
for (WorkSpec workSpec : workSpecs) {
capturedIds.add(workSpec.id);
}
assertThat(capturedIds.size(), is(2));
assertThat(capturedIds.contains(noConstraints.getStringId()), is(true));
assertThat(capturedIds.contains(workWithConstraints.getStringId()), is(true));
assertThat(capturedIds.contains(failed.getStringId()), is(false));
assertThat(capturedIds.contains(succeeded.getStringId()), is(false));
}
private static List<String> intentActionsFor(@NonNull List<Intent> intents) {
List<String> intentActions = new ArrayList<>(intents.size());
for (Intent intent : intents) {
intentActions.add(intent.getAction());
}
return intentActions;
}
// Marking it public for mocking
public static class CommandInterceptingSystemDispatcher extends SystemAlarmDispatcher {
private final List<Intent> mCommands;
private final Map<String, Integer> mActionCount;
CommandInterceptingSystemDispatcher(@NonNull Context context,
@Nullable Processor processor,
@Nullable WorkManagerImpl workManager) {
super(context, processor, workManager);
mCommands = new ArrayList<>();
mActionCount = new HashMap<>();
}
@Override
public boolean add(@NonNull Intent intent, int startId) {
boolean isAdded = super.add(intent, startId);
if (isAdded) {
update(intent);
}
return isAdded;
}
private void update(Intent intent) {
String action = intent.getAction();
Integer count = mActionCount.get(intent.getAction());
int incremented = count != null ? count + 1 : 1;
mActionCount.put(action, incremented);
mCommands.add(intent);
}
Map<String, Integer> getActionCount() {
return mActionCount;
}
List<Intent> getCommands() {
return mCommands;
}
}
}