1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.service.measurement.registration; 18 19 import static com.android.adservices.service.measurement.util.JobLockHolder.Type.ASYNC_REGISTRATION_PROCESSING; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON; 21 import static com.android.adservices.spe.AdServicesJobInfo.MEASUREMENT_ASYNC_REGISTRATION_JOB; 22 23 import android.app.job.JobInfo; 24 import android.app.job.JobParameters; 25 import android.app.job.JobScheduler; 26 import android.app.job.JobService; 27 import android.content.ComponentName; 28 import android.content.Context; 29 30 import com.android.adservices.LogUtil; 31 import com.android.adservices.LoggerFactory; 32 import com.android.adservices.concurrency.AdServicesExecutors; 33 import com.android.adservices.service.Flags; 34 import com.android.adservices.service.FlagsFactory; 35 import com.android.adservices.service.common.compat.ServiceCompatUtils; 36 import com.android.adservices.service.measurement.registration.AsyncRegistrationQueueRunner.ProcessingResult; 37 import com.android.adservices.service.measurement.util.JobLockHolder; 38 import com.android.adservices.spe.AdServicesJobServiceLogger; 39 import com.android.internal.annotations.VisibleForTesting; 40 41 import java.time.Clock; 42 import java.time.Instant; 43 import java.util.concurrent.Future; 44 45 /** Job Service for servicing queued registration requests */ 46 public class AsyncRegistrationQueueJobService extends JobService { 47 private static final int MEASUREMENT_ASYNC_REGISTRATION_JOB_ID = 48 MEASUREMENT_ASYNC_REGISTRATION_JOB.getJobId(); 49 private Future mExecutorFuture; 50 51 @Override onStartJob(JobParameters params)52 public boolean onStartJob(JobParameters params) { 53 // Always ensure that the first thing this job does is check if it should be running, and 54 // cancel itself if it's not supposed to be. 55 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 56 LogUtil.d( 57 "Disabling AsyncRegistrationQueueJobService job because it's running in" 58 + " ExtServices on T+"); 59 return skipAndCancelBackgroundJob(params, /* skipReason=*/ 0, /* doRecord=*/ false); 60 } 61 62 AdServicesJobServiceLogger.getInstance() 63 .recordOnStartJob(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID); 64 65 if (FlagsFactory.getFlags().getAsyncRegistrationJobQueueKillSwitch()) { 66 LoggerFactory.getMeasurementLogger().e("AsyncRegistrationQueueJobService is disabled"); 67 return skipAndCancelBackgroundJob( 68 params, 69 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 70 /* doRecord=*/ true); 71 } 72 73 Instant jobStartTime = Clock.systemUTC().instant(); 74 LoggerFactory.getMeasurementLogger() 75 .d( 76 "AsyncRegistrationQueueJobService.onStartJob " + "at %s", 77 jobStartTime.toString()); 78 79 mExecutorFuture = 80 AdServicesExecutors.getBlockingExecutor() 81 .submit( 82 () -> { 83 ProcessingResult result = processAsyncRecords(); 84 LoggerFactory.getMeasurementLogger() 85 .d( 86 "AsyncRegistrationQueueJobService finished" 87 + " processing [%s]", 88 result); 89 90 final boolean shouldRetry = 91 !ProcessingResult.SUCCESS_ALL_RECORDS_PROCESSED.equals( 92 result); 93 final boolean isSuccessful = 94 !ProcessingResult.THREAD_INTERRUPTED.equals(result); 95 AdServicesJobServiceLogger.getInstance() 96 .recordJobFinished( 97 MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, 98 isSuccessful, 99 shouldRetry); 100 101 switch (result) { 102 case SUCCESS_ALL_RECORDS_PROCESSED: 103 // Force scheduling to avoid concurrency issue 104 scheduleIfNeeded(this, /* forceSchedule */ true); 105 break; 106 case SUCCESS_WITH_PENDING_RECORDS: 107 scheduleImmediately( 108 AsyncRegistrationQueueJobService.this); 109 break; 110 case THREAD_INTERRUPTED: 111 default: 112 // Reschedule with back-off criteria specified when it 113 // was 114 // scheduled 115 jobFinished(params, /* wantsReschedule= */ true); 116 } 117 }); 118 return true; 119 } 120 121 @VisibleForTesting processAsyncRecords()122 ProcessingResult processAsyncRecords() { 123 final JobLockHolder lock = JobLockHolder.getInstance(ASYNC_REGISTRATION_PROCESSING); 124 if (lock.tryLock()) { 125 try { 126 return AsyncRegistrationQueueRunner.getInstance(getApplicationContext()) 127 .runAsyncRegistrationQueueWorker(); 128 } finally { 129 lock.unlock(); 130 } 131 } 132 LoggerFactory.getMeasurementLogger() 133 .d("AsyncRegistrationQueueJobService did not acquire the lock"); 134 // Another thread is already processing async registrations. 135 return ProcessingResult.SUCCESS_ALL_RECORDS_PROCESSED; 136 } 137 138 @Override onStopJob(JobParameters params)139 public boolean onStopJob(JobParameters params) { 140 LoggerFactory.getMeasurementLogger().d("AsyncRegistrationQueueJobService.onStopJob"); 141 boolean shouldRetry = true; 142 if (mExecutorFuture != null) { 143 shouldRetry = mExecutorFuture.cancel(/* mayInterruptIfRunning */ true); 144 } 145 AdServicesJobServiceLogger.getInstance() 146 .recordOnStopJob(params, MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, shouldRetry); 147 return shouldRetry; 148 } 149 150 @VisibleForTesting schedule(JobScheduler jobScheduler, JobInfo job)151 protected static void schedule(JobScheduler jobScheduler, JobInfo job) { 152 jobScheduler.schedule(job); 153 } 154 buildJobInfo(Context context, Flags flags)155 private static JobInfo buildJobInfo(Context context, Flags flags) { 156 return new JobInfo.Builder( 157 MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, 158 new ComponentName(context, AsyncRegistrationQueueJobService.class)) 159 .addTriggerContentUri( 160 new JobInfo.TriggerContentUri( 161 AsyncRegistrationContentProvider.TRIGGER_URI, 162 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)) 163 .setTriggerContentUpdateDelay( 164 flags.getMeasurementAsyncRegistrationJobTriggerMinDelayMs()) 165 .setTriggerContentMaxDelay( 166 flags.getMeasurementAsyncRegistrationJobTriggerMaxDelayMs()) 167 .setRequiredNetworkType( 168 flags.getMeasurementAsyncRegistrationQueueJobRequiredNetworkType()) 169 // Can't call addTriggerContentUri() on a persisted job 170 .setPersisted(flags.getMeasurementAsyncRegistrationQueueJobPersisted()) 171 .build(); 172 } 173 174 /** 175 * Schedule Async Registration Queue Job Service if it is not already scheduled 176 * 177 * @param context the context 178 * @param forceSchedule flag to indicate whether to force rescheduling the job. 179 */ scheduleIfNeeded(Context context, boolean forceSchedule)180 public static void scheduleIfNeeded(Context context, boolean forceSchedule) { 181 Flags flags = FlagsFactory.getFlags(); 182 if (flags.getAsyncRegistrationJobQueueKillSwitch()) { 183 LoggerFactory.getMeasurementLogger() 184 .e("AsyncRegistrationQueueJobService is disabled, skip scheduling"); 185 return; 186 } 187 188 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 189 if (jobScheduler == null) { 190 LoggerFactory.getMeasurementLogger().e("JobScheduler not found"); 191 return; 192 } 193 194 final JobInfo scheduledJobInfo = 195 jobScheduler.getPendingJob(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID); 196 // Schedule if it hasn't been scheduled already or force rescheduling 197 JobInfo jobInfo = buildJobInfo(context, flags); 198 if (forceSchedule || !jobInfo.equals(scheduledJobInfo)) { 199 schedule(jobScheduler, jobInfo); 200 LoggerFactory.getMeasurementLogger().d("Scheduled AsyncRegistrationQueueJobService"); 201 } else { 202 LoggerFactory.getMeasurementLogger() 203 .d("AsyncRegistrationQueueJobService already scheduled, skipping reschedule"); 204 } 205 } 206 207 @VisibleForTesting scheduleImmediately(Context context)208 void scheduleImmediately(Context context) { 209 Flags flags = FlagsFactory.getFlags(); 210 if (flags.getAsyncRegistrationJobQueueKillSwitch()) { 211 LoggerFactory.getMeasurementLogger() 212 .e("AsyncRegistrationQueueJobService is disabled, skip scheduling"); 213 return; 214 } 215 216 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 217 if (jobScheduler == null) { 218 LoggerFactory.getMeasurementLogger().e("JobScheduler not found"); 219 return; 220 } 221 222 final JobInfo job = 223 new JobInfo.Builder( 224 MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, 225 new ComponentName(context, AsyncRegistrationQueueJobService.class)) 226 .setRequiredNetworkType( 227 flags.getMeasurementAsyncRegistrationQueueJobRequiredNetworkType()) 228 .build(); 229 230 schedule(jobScheduler, job); 231 LoggerFactory.getMeasurementLogger() 232 .d("AsyncRegistrationQueueJobService scheduled to run immediately"); 233 } 234 skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)235 private boolean skipAndCancelBackgroundJob( 236 final JobParameters params, int skipReason, boolean doRecord) { 237 final JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 238 if (jobScheduler != null) { 239 jobScheduler.cancel(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID); 240 } 241 242 if (doRecord) { 243 AdServicesJobServiceLogger.getInstance() 244 .recordJobSkipped(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, skipReason); 245 } 246 247 // Tell the JobScheduler that the job is done and does not need to be rescheduled 248 jobFinished(params, false); 249 250 // Returning false to reschedule this job. 251 return false; 252 } 253 254 @VisibleForTesting getFutureForTesting()255 Future getFutureForTesting() { 256 return mExecutorFuture; 257 } 258 } 259