blob: 837f45471f82eebe22c509cb0851a0432a6c4789 [file] [log] [blame]
/*
* Copyright (C) 2022 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.adservices.service;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_API_DISABLED;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS;
import static com.android.adservices.spe.AdservicesJobInfo.MAINTENANCE_JOB;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
import com.android.adservices.data.adselection.AdSelectionDatabase;
import com.android.adservices.data.adselection.AdSelectionEntryDao;
import com.android.adservices.errorlogging.ErrorLogUtil;
import com.android.adservices.service.common.FledgeMaintenanceTasksWorker;
import com.android.adservices.service.common.compat.ServiceCompatUtils;
import com.android.adservices.service.stats.Clock;
import com.android.adservices.service.stats.StatsdAdServicesLogger;
import com.android.adservices.service.topics.AppUpdateManager;
import com.android.adservices.service.topics.BlockedTopicsManager;
import com.android.adservices.service.topics.CacheManager;
import com.android.adservices.service.topics.EpochJobService;
import com.android.adservices.service.topics.EpochManager;
import com.android.adservices.service.topics.TopicsWorker;
import com.android.adservices.spe.AdservicesJobServiceLogger;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.Spy;
import org.mockito.quality.Strictness;
/** Unit tests for {@link com.android.adservices.service.MaintenanceJobService} */
@SuppressWarnings("ConstantConditions")
public class MaintenanceJobServiceTest {
private static final int BACKGROUND_THREAD_TIMEOUT_MS = 5_000;
private static final int MAINTENANCE_JOB_ID = MAINTENANCE_JOB.getJobId();
private static final long MAINTENANCE_JOB_PERIOD_MS = 10_000L;
private static final long MAINTENANCE_JOB_FLEX_MS = 1_000L;
private static final long CURRENT_EPOCH_ID = 1L;
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
private static final JobScheduler JOB_SCHEDULER = CONTEXT.getSystemService(JobScheduler.class);
private static final Flags TEST_FLAGS = FlagsFactory.getFlagsForTest();
@Spy private MaintenanceJobService mSpyMaintenanceJobService;
private MockitoSession mStaticMockSession;
// Mock EpochManager and CacheManager as the methods called are tested in corresponding
// unit test. In this test, only verify whether specific method is initiated.
@Mock EpochManager mMockEpochManager;
@Mock CacheManager mMockCacheManager;
@Mock BlockedTopicsManager mBlockedTopicsManager;
@Mock AppUpdateManager mMockAppUpdateManager;
@Mock JobParameters mMockJobParameters;
@Mock Flags mMockFlags;
@Mock JobScheduler mMockJobScheduler;
@Mock StatsdAdServicesLogger mMockStatsdLogger;
private AdservicesJobServiceLogger mSpyLogger;
@Spy private FledgeMaintenanceTasksWorker mFledgeMaintenanceTasksWorkerSpy;
@Before
public void setup() {
AdSelectionEntryDao adSelectionEntryDao =
Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AdSelectionDatabase.class)
.build()
.adSelectionEntryDao();
mFledgeMaintenanceTasksWorkerSpy = new FledgeMaintenanceTasksWorker(adSelectionEntryDao);
// Start a mockitoSession to mock static method
mStaticMockSession =
ExtendedMockito.mockitoSession()
.spyStatic(MaintenanceJobService.class)
.spyStatic(TopicsWorker.class)
.spyStatic(FlagsFactory.class)
.spyStatic(AdservicesJobServiceLogger.class)
.spyStatic(ErrorLogUtil.class)
.mockStatic(ServiceCompatUtils.class)
.initMocks(this)
.strictness(Strictness.LENIENT)
.startMocking();
// Mock JobScheduler invocation in EpochJobService
assertThat(JOB_SCHEDULER).isNotNull();
ExtendedMockito.doReturn(JOB_SCHEDULER)
.when(mSpyMaintenanceJobService)
.getSystemService(JobScheduler.class);
// Mock AdservicesJobServiceLogger to not actually log the stats to server
mSpyLogger =
spy(new AdservicesJobServiceLogger(CONTEXT, Clock.SYSTEM_CLOCK, mMockStatsdLogger));
Mockito.doNothing()
.when(mSpyLogger)
.logExecutionStats(anyInt(), anyLong(), anyInt(), anyInt());
ExtendedMockito.doReturn(mSpyLogger)
.when(() -> AdservicesJobServiceLogger.getInstance(any(Context.class)));
}
@After
public void teardown() {
JOB_SCHEDULER.cancelAll();
if (mStaticMockSession != null) {
mStaticMockSession.finishMocking();
}
}
@Test
public void testOnStartJob_killSwitchOff_withoutLogging() throws InterruptedException {
// Logging killswitch is on.
Mockito.doReturn(true).when(mMockFlags).getBackgroundJobsLoggingKillSwitch();
testOnStartJob_killSwitchOff();
// Verify logging methods are not invoked.
verify(mSpyLogger, never()).persistJobExecutionData(anyInt(), anyLong());
verify(mSpyLogger, never()).logExecutionStats(anyInt(), anyLong(), anyInt(), anyInt());
}
@Test
public void testOnStartJob_killSwitchOff_withLogging() throws InterruptedException {
// Logging killswitch is off.
Mockito.doReturn(false).when(mMockFlags).getBackgroundJobsLoggingKillSwitch();
testOnStartJob_killSwitchOff();
// Verify logging methods are invoked.
verify(mSpyLogger, atLeastOnce()).persistJobExecutionData(anyInt(), anyLong());
}
@Test
public void testOnStartJob_TopicsKillSwitchOn() throws InterruptedException {
// Killswitch is off.
doReturn(true).when(mMockFlags).getTopicsKillSwitch();
doReturn(false).when(mMockFlags).getFledgeSelectAdsKillSwitch();
doReturn(CURRENT_EPOCH_ID).when(mMockEpochManager).getCurrentEpochId();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
doReturn(TEST_FLAGS.getAdSelectionExpirationWindowS())
.when(mMockFlags)
.getAdSelectionExpirationWindowS();
// Inject FledgeMaintenanceTasksWorker since the test can't get it the standard way
mSpyMaintenanceJobService.injectFledgeMaintenanceTasksWorker(
mFledgeMaintenanceTasksWorkerSpy);
doNothing().when(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
// Schedule the job to assert after starting that the scheduled job has been started
JobInfo existingJobInfo =
new JobInfo.Builder(
MAINTENANCE_JOB_ID,
new ComponentName(CONTEXT, EpochJobService.class))
.setRequiresCharging(true)
.setPeriodic(MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS)
.setPersisted(true)
.build();
JOB_SCHEDULER.schedule(existingJobInfo);
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Now verify that when the Job starts, it will schedule itself.
assertThat(mSpyMaintenanceJobService.onStartJob(mMockJobParameters)).isTrue();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Verify that topics job is not done
ExtendedMockito.verify(() -> TopicsWorker.getInstance(any(Context.class)), never());
verify(mMockAppUpdateManager, never())
.reconcileUninstalledApps(any(Context.class), eq(CURRENT_EPOCH_ID));
verify(mMockAppUpdateManager, never())
.reconcileInstalledApps(any(Context.class), /* currentEpochId */ anyLong());
// Verifying that FLEDGE job was done
verify(mFledgeMaintenanceTasksWorkerSpy).clearExpiredAdSelectionData();
}
@Test
public void testOnStartJob_SelectAdsKillSwitchOn() throws InterruptedException {
final TopicsWorker topicsWorker =
new TopicsWorker(
mMockEpochManager,
mMockCacheManager,
mBlockedTopicsManager,
mMockAppUpdateManager,
TEST_FLAGS);
// Killswitch is off.
doReturn(false).when(mMockFlags).getTopicsKillSwitch();
doReturn(true).when(mMockFlags).getFledgeSelectAdsKillSwitch();
doReturn(CURRENT_EPOCH_ID).when(mMockEpochManager).getCurrentEpochId();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// Mock static method AppUpdateWorker.getInstance, let it return the local
// appUpdateWorker in order to get a test instance.
ExtendedMockito.doReturn(topicsWorker)
.when(() -> TopicsWorker.getInstance(any(Context.class)));
doNothing().when(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
// Schedule the job to assert after starting that the scheduled job has been started
JobInfo existingJobInfo =
new JobInfo.Builder(
MAINTENANCE_JOB_ID,
new ComponentName(CONTEXT, EpochJobService.class))
.setRequiresCharging(true)
.setPeriodic(MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS)
.setPersisted(true)
.build();
JOB_SCHEDULER.schedule(existingJobInfo);
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Now verify that when the Job starts, it will schedule itself.
assertThat(mSpyMaintenanceJobService.onStartJob(mMockJobParameters)).isTrue();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
ExtendedMockito.verify(() -> TopicsWorker.getInstance(any(Context.class)));
verify(mMockAppUpdateManager)
.reconcileUninstalledApps(any(Context.class), eq(CURRENT_EPOCH_ID));
verify(mMockAppUpdateManager)
.reconcileInstalledApps(any(Context.class), /* currentEpochId */ anyLong());
verify(mFledgeMaintenanceTasksWorkerSpy, never()).clearExpiredAdSelectionData();
}
@Test
public void testOnStartJob_killSwitchOn_withoutLogging() {
// Logging killswitch is on.
Mockito.doReturn(true).when(mMockFlags).getBackgroundJobsLoggingKillSwitch();
testOnStartJob_killSwitchOn();
// Verify logging methods are not invoked.
verify(mSpyLogger, never()).persistJobExecutionData(anyInt(), anyLong());
verify(mSpyLogger, never()).logExecutionStats(anyInt(), anyLong(), anyInt(), anyInt());
}
@Test
public void testOnStartJob_killSwitchOn_withLogging() {
// Logging killswitch is off.
Mockito.doReturn(false).when(mMockFlags).getBackgroundJobsLoggingKillSwitch();
testOnStartJob_killSwitchOn();
// Verify logging methods are invoked.
verify(mSpyLogger).persistJobExecutionData(anyInt(), anyLong());
verify(mSpyLogger)
.logExecutionStats(
anyInt(),
anyLong(),
eq(
AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON),
anyInt());
}
@Test
public void testOnStartJob_killSwitchOnDoesFledgeJobWhenTopicsJobThrowsException()
throws Exception {
final TopicsWorker topicsWorker =
new TopicsWorker(
mMockEpochManager,
mMockCacheManager,
mBlockedTopicsManager,
mMockAppUpdateManager,
TEST_FLAGS);
// Killswitch is off.
doReturn(false).when(mMockFlags).getTopicsKillSwitch();
doReturn(false).when(mMockFlags).getFledgeSelectAdsKillSwitch();
doReturn(CURRENT_EPOCH_ID).when(mMockEpochManager).getCurrentEpochId();
doReturn(TEST_FLAGS.getAdSelectionExpirationWindowS())
.when(mMockFlags)
.getAdSelectionExpirationWindowS();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// Mock static method AppUpdateWorker.getInstance, let it return the local
// appUpdateWorker in order to get a test instance.
ExtendedMockito.doReturn(topicsWorker)
.when(() -> TopicsWorker.getInstance(any(Context.class)));
// Inject FledgeMaintenanceTasksWorker since the test can't get it the standard way
mSpyMaintenanceJobService.injectFledgeMaintenanceTasksWorker(
mFledgeMaintenanceTasksWorkerSpy);
// Simulating a failure in Topics job
ExtendedMockito.doThrow(new IllegalStateException())
.when(mMockAppUpdateManager)
.reconcileUninstalledApps(any(Context.class), eq(CURRENT_EPOCH_ID));
doNothing().when(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
// Schedule the job to assert after starting that the scheduled job has been started
JobInfo existingJobInfo =
new JobInfo.Builder(
MAINTENANCE_JOB_ID,
new ComponentName(CONTEXT, EpochJobService.class))
.setRequiresCharging(true)
.setPeriodic(MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS)
.setPersisted(true)
.build();
JOB_SCHEDULER.schedule(existingJobInfo);
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Now verify that when the Job starts, it will schedule itself.
assertThat(mSpyMaintenanceJobService.onStartJob(mMockJobParameters)).isTrue();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Verify that this is not called because we threw an exception
verify(mMockAppUpdateManager, never())
.reconcileInstalledApps(any(Context.class), /* currentEpochId */ anyLong());
// Verifying that FLEDGE job was done
verify(mFledgeMaintenanceTasksWorkerSpy).clearExpiredAdSelectionData();
}
@Test
public void testOnStartJob_killSwitchOnDoesTopicsJobWhenFledgeThrowsException()
throws Exception {
final TopicsWorker topicsWorker =
new TopicsWorker(
mMockEpochManager,
mMockCacheManager,
mBlockedTopicsManager,
mMockAppUpdateManager,
TEST_FLAGS);
// Killswitch is off.
doReturn(false).when(mMockFlags).getTopicsKillSwitch();
doReturn(false).when(mMockFlags).getFledgeSelectAdsKillSwitch();
doReturn(CURRENT_EPOCH_ID).when(mMockEpochManager).getCurrentEpochId();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// Mock static method AppUpdateWorker.getInstance, let it return the local
// appUpdateWorker in order to get a test instance.
ExtendedMockito.doReturn(topicsWorker)
.when(() -> TopicsWorker.getInstance(any(Context.class)));
// Inject FledgeMaintenanceTasksWorker since the test can't get it the standard way
mSpyMaintenanceJobService.injectFledgeMaintenanceTasksWorker(
mFledgeMaintenanceTasksWorkerSpy);
// Simulating a failure in Fledge job
ExtendedMockito.doThrow(new IllegalStateException())
.when(mFledgeMaintenanceTasksWorkerSpy)
.clearExpiredAdSelectionData();
doNothing().when(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
// Schedule the job to assert after starting that the scheduled job has been started
JobInfo existingJobInfo =
new JobInfo.Builder(
MAINTENANCE_JOB_ID,
new ComponentName(CONTEXT, EpochJobService.class))
.setRequiresCharging(true)
.setPeriodic(MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS)
.setPersisted(true)
.build();
JOB_SCHEDULER.schedule(existingJobInfo);
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Now verify that when the Job starts, it will schedule itself.
assertThat(mSpyMaintenanceJobService.onStartJob(mMockJobParameters)).isTrue();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
ExtendedMockito.verify(() -> TopicsWorker.getInstance(any(Context.class)));
verify(mMockAppUpdateManager)
.reconcileUninstalledApps(any(Context.class), eq(CURRENT_EPOCH_ID));
verify(mMockAppUpdateManager)
.reconcileInstalledApps(any(Context.class), /* currentEpochId */ anyLong());
verify(mFledgeMaintenanceTasksWorkerSpy).clearExpiredAdSelectionData();
}
@Test
public void testOnStartJob_globalKillswitchOverridesAll() throws InterruptedException {
Flags flagsGlobalKillSwitchOn =
new Flags() {
@Override
public boolean getGlobalKillSwitch() {
return true;
}
};
// Setting mock flags to use flags with global switch overridden
doReturn(flagsGlobalKillSwitchOn.getTopicsKillSwitch())
.when(mMockFlags)
.getTopicsKillSwitch();
doReturn(flagsGlobalKillSwitchOn.getTopicsKillSwitch())
.when(mMockFlags)
.getFledgeSelectAdsKillSwitch();
doReturn(CURRENT_EPOCH_ID).when(mMockEpochManager).getCurrentEpochId();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
ExtendedMockito.doNothing()
.when(mSpyMaintenanceJobService)
.jobFinished(mMockJobParameters, false);
doNothing().when(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
// Schedule the job to assert after starting that the scheduled job has been cancelled
JobInfo existingJobInfo =
new JobInfo.Builder(
MAINTENANCE_JOB_ID,
new ComponentName(CONTEXT, EpochJobService.class))
.setRequiresCharging(true)
.setPeriodic(MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS)
.setPersisted(true)
.build();
JOB_SCHEDULER.schedule(existingJobInfo);
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Now verify that when the Job starts, it will unschedule itself.
assertThat(mSpyMaintenanceJobService.onStartJob(mMockJobParameters)).isFalse();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNull();
verify(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
verifyNoMoreInteractions(staticMockMarker(TopicsWorker.class));
verify(() -> TopicsWorker.getInstance(any(Context.class)), never());
verify(mMockAppUpdateManager, never())
.reconcileUninstalledApps(any(Context.class), eq(CURRENT_EPOCH_ID));
verify(mMockAppUpdateManager, never())
.reconcileInstalledApps(any(Context.class), /* currentEpochId */ anyLong());
// Ensure Fledge job not was done
verify(mFledgeMaintenanceTasksWorkerSpy, never()).clearExpiredAdSelectionData();
}
@Test
public void testOnStopJob_withoutLogging() {
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// Logging killswitch is on.
Mockito.doReturn(true).when(mMockFlags).getBackgroundJobsLoggingKillSwitch();
testOnStopJob();
// Verify logging methods are not invoked.
verify(mSpyLogger, never()).persistJobExecutionData(anyInt(), anyLong());
verify(mSpyLogger, never()).logExecutionStats(anyInt(), anyLong(), anyInt(), anyInt());
}
@Test
public void testOnStopJob_withLogging() {
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// Logging killswitch is off.
Mockito.doReturn(false).when(mMockFlags).getBackgroundJobsLoggingKillSwitch();
testOnStopJob();
// Verify logging methods are invoked.
verify(mSpyLogger).logExecutionStats(anyInt(), anyLong(), anyInt(), anyInt());
}
@Test
public void testScheduleIfNeeded_Success() {
doReturn(false).when(mMockFlags).getGlobalKillSwitch();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// The first invocation of scheduleIfNeeded() schedules the job.
assertThat(MaintenanceJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ false))
.isTrue();
}
@Test
public void testScheduleIfNeeded_ScheduledWithSameParameters() {
// Mock Flags in order to change values within this test
doReturn(TEST_FLAGS.getMaintenanceJobPeriodMs())
.when(mMockFlags)
.getMaintenanceJobPeriodMs();
doReturn(TEST_FLAGS.getMaintenanceJobFlexMs()).when(mMockFlags).getMaintenanceJobFlexMs();
doReturn(false).when(mMockFlags).getGlobalKillSwitch();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// The first invocation of scheduleIfNeeded() schedules the job.
assertThat(MaintenanceJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ false))
.isTrue();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// The second invocation of scheduleIfNeeded() with same parameters skips the scheduling.
assertThat(MaintenanceJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ false))
.isFalse();
}
@Test
public void testScheduleIfNeeded_ScheduledWithDifferentParameters() {
// Mock Flags in order to change values within this test
doReturn(TEST_FLAGS.getMaintenanceJobPeriodMs())
.when(mMockFlags)
.getMaintenanceJobPeriodMs();
doReturn(TEST_FLAGS.getMaintenanceJobFlexMs()).when(mMockFlags).getMaintenanceJobFlexMs();
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// The first invocation of scheduleIfNeeded() schedules the job.
assertThat(MaintenanceJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ false))
.isTrue();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Change the value of a parameter so that the second invocation of scheduleIfNeeded()
// schedules the job.
doReturn(TEST_FLAGS.getMaintenanceJobFlexMs() + 1)
.when(mMockFlags)
.getMaintenanceJobFlexMs();
assertThat(MaintenanceJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ false))
.isTrue();
}
@Test
public void testScheduleIfNeeded_forceRun() {
// Mock Flags in order to change values within this test
doReturn(TEST_FLAGS.getMaintenanceJobPeriodMs())
.when(mMockFlags)
.getMaintenanceJobPeriodMs();
doReturn(TEST_FLAGS.getMaintenanceJobFlexMs()).when(mMockFlags).getMaintenanceJobFlexMs();
doReturn(false).when(mMockFlags).getGlobalKillSwitch();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// The first invocation of scheduleIfNeeded() schedules the job.
assertThat(MaintenanceJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ false))
.isTrue();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// The second invocation of scheduleIfNeeded() with same parameters skips the scheduling.
assertThat(MaintenanceJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ false))
.isFalse();
// The third invocation of scheduleIfNeeded() is forced and re-schedules the job.
assertThat(MaintenanceJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ true))
.isTrue();
}
@Test
public void testScheduleIfNeeded_scheduledWithKillSwitchOn() {
ExtendedMockito.doNothing()
.when(() -> ErrorLogUtil.e(anyInt(), anyInt(), anyString(), anyString()));
// Killswitch is on.
doReturn(true).when(mMockFlags).getTopicsKillSwitch();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// The first invocation of scheduleIfNeeded() schedules the job.
assertThat(EpochJobService.scheduleIfNeeded(CONTEXT, /* forceSchedule */ false)).isFalse();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNull();
ExtendedMockito.verify(
()-> {
ErrorLogUtil.e(
eq(AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_API_DISABLED),
eq(AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS),
anyString(),
anyString());
}
);
}
@Test
public void testSchedule_jobInfoIsPersisted() {
final ArgumentCaptor<JobInfo> argumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
MaintenanceJobService.schedule(
CONTEXT, mMockJobScheduler, MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS);
verify(mMockJobScheduler, times(1)).schedule(argumentCaptor.capture());
assertThat(argumentCaptor.getValue()).isNotNull();
assertThat(argumentCaptor.getValue().isPersisted()).isTrue();
}
@Test
public void testOnStartJob_shouldDisableJobTrue_withoutLogging() {
// Logging killswitch is on.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
Mockito.doReturn(true).when(mMockFlags).getBackgroundJobsLoggingKillSwitch();
testOnStartJob_shouldDisableJobTrue();
// Verify logging method is not invoked.
verify(mSpyLogger, never()).logExecutionStats(anyInt(), anyLong(), anyInt(), anyInt());
}
@Test
public void testOnStartJob_shouldDisableJobTrue_withLoggingEnabled() {
// Logging killswitch is off.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
Mockito.doReturn(false).when(mMockFlags).getBackgroundJobsLoggingKillSwitch();
testOnStartJob_shouldDisableJobTrue();
// Verify logging has not happened even though logging is enabled because this field is not
// logged
verify(mSpyLogger, never()).logExecutionStats(anyInt(), anyLong(), anyInt(), anyInt());
}
private void testOnStartJob_killSwitchOn() {
// Killswitch on.
doReturn(true).when(mMockFlags).getTopicsKillSwitch();
doReturn(true).when(mMockFlags).getFledgeSelectAdsKillSwitch();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
doNothing().when(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
// Inject FledgeMaintenanceTasksWorker since the test can't get it the standard way
mSpyMaintenanceJobService.injectFledgeMaintenanceTasksWorker(
mFledgeMaintenanceTasksWorkerSpy);
// Schedule the job to assert after starting that the scheduled job has been cancelled
JobInfo existingJobInfo =
new JobInfo.Builder(
MAINTENANCE_JOB_ID,
new ComponentName(CONTEXT, EpochJobService.class))
.setRequiresCharging(true)
.setPeriodic(MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS)
.setPersisted(true)
.build();
JOB_SCHEDULER.schedule(existingJobInfo);
assertNotNull(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID));
// Now verify that when the Job starts, it will unschedule itself.
assertFalse(mSpyMaintenanceJobService.onStartJob(mMockJobParameters));
assertNull(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID));
verify(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
verifyNoMoreInteractions(staticMockMarker(TopicsWorker.class));
verify(mFledgeMaintenanceTasksWorkerSpy, never()).clearExpiredAdSelectionData();
}
private void testOnStartJob_killSwitchOff() throws InterruptedException {
final TopicsWorker topicsWorker =
new TopicsWorker(
mMockEpochManager,
mMockCacheManager,
mBlockedTopicsManager,
mMockAppUpdateManager,
TEST_FLAGS);
// Killswitch is off.
doReturn(false).when(mMockFlags).getTopicsKillSwitch();
doReturn(false).when(mMockFlags).getFledgeSelectAdsKillSwitch();
doReturn(CURRENT_EPOCH_ID).when(mMockEpochManager).getCurrentEpochId();
doReturn(TEST_FLAGS.getAdSelectionExpirationWindowS())
.when(mMockFlags)
.getAdSelectionExpirationWindowS();
// Mock static method FlagsFactory.getFlags() to return Mock Flags.
ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
// Mock static method AppUpdateWorker.getInstance, let it return the local
// appUpdateWorker in order to get a test instance.
ExtendedMockito.doReturn(topicsWorker)
.when(() -> TopicsWorker.getInstance(any(Context.class)));
doNothing().when(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
mSpyMaintenanceJobService.injectFledgeMaintenanceTasksWorker(
mFledgeMaintenanceTasksWorkerSpy);
// Schedule the job to assert after starting that the scheduled job has been started
JobInfo existingJobInfo =
new JobInfo.Builder(
MAINTENANCE_JOB_ID,
new ComponentName(CONTEXT, EpochJobService.class))
.setRequiresCharging(true)
.setPeriodic(MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS)
.setPersisted(true)
.build();
JOB_SCHEDULER.schedule(existingJobInfo);
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
// Now verify that when the Job starts, it will schedule itself.
assertThat(mSpyMaintenanceJobService.onStartJob(mMockJobParameters)).isTrue();
assertThat(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID)).isNotNull();
ExtendedMockito.verify(() -> TopicsWorker.getInstance(any(Context.class)));
verify(mMockAppUpdateManager)
.reconcileUninstalledApps(any(Context.class), eq(CURRENT_EPOCH_ID));
verify(mMockAppUpdateManager)
.reconcileInstalledApps(any(Context.class), /* currentEpochId */ anyLong());
// Ensure Fledge job was done
verify(mFledgeMaintenanceTasksWorkerSpy).clearExpiredAdSelectionData();
}
private void testOnStopJob() {
// Verify nothing throws
mSpyMaintenanceJobService.onStopJob(mMockJobParameters);
}
private void testOnStartJob_shouldDisableJobTrue() {
ExtendedMockito.doReturn(true)
.when(
() ->
ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(
any(Context.class)));
doNothing().when(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
// Inject FledgeMaintenanceTasksWorker since the test can't get it the standard way
mSpyMaintenanceJobService.injectFledgeMaintenanceTasksWorker(
mFledgeMaintenanceTasksWorkerSpy);
// Schedule the job to assert after starting that the scheduled job has been cancelled
JobInfo existingJobInfo =
new JobInfo.Builder(
MAINTENANCE_JOB_ID,
new ComponentName(CONTEXT, EpochJobService.class))
.setRequiresCharging(true)
.setPeriodic(MAINTENANCE_JOB_PERIOD_MS, MAINTENANCE_JOB_FLEX_MS)
.setPersisted(true)
.build();
JOB_SCHEDULER.schedule(existingJobInfo);
assertNotNull(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID));
// Now verify that when the Job starts, it will unschedule itself.
assertFalse(mSpyMaintenanceJobService.onStartJob(mMockJobParameters));
assertNull(JOB_SCHEDULER.getPendingJob(MAINTENANCE_JOB_ID));
verify(mSpyMaintenanceJobService).jobFinished(mMockJobParameters, false);
verifyNoMoreInteractions(staticMockMarker(TopicsWorker.class));
verify(mFledgeMaintenanceTasksWorkerSpy, never()).clearExpiredAdSelectionData();
}
}