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 import android.os.Trace; 30 31 import com.android.adservices.LogUtil; 32 import com.android.adservices.LoggerFactory; 33 import com.android.adservices.concurrency.AdServicesExecutors; 34 import com.android.adservices.service.Flags; 35 import com.android.adservices.service.FlagsFactory; 36 import com.android.adservices.service.common.compat.ServiceCompatUtils; 37 import com.android.adservices.service.measurement.registration.AsyncRegistrationQueueRunner.ProcessingResult; 38 import com.android.adservices.service.measurement.reporting.DebugReportingJobService; 39 import com.android.adservices.service.measurement.util.JobLockHolder; 40 import com.android.adservices.spe.AdServicesJobInfo; 41 import com.android.adservices.spe.AdServicesJobServiceLogger; 42 import com.android.internal.annotations.VisibleForTesting; 43 44 import java.time.Clock; 45 import java.time.Instant; 46 import java.util.concurrent.Future; 47 48 /** Job Service for servicing queued registration requests */ 49 public class AsyncRegistrationQueueJobService extends JobService { 50 private static final int MEASUREMENT_ASYNC_REGISTRATION_JOB_ID = 51 MEASUREMENT_ASYNC_REGISTRATION_JOB.getJobId(); 52 private Future mExecutorFuture; 53 54 @Override onStartJob(JobParameters params)55 public boolean onStartJob(JobParameters params) { 56 // Always ensure that the first thing this job does is check if it should be running, and 57 // cancel itself if it's not supposed to be. 58 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 59 LogUtil.d( 60 "Disabling AsyncRegistrationQueueJobService job because it's running in" 61 + " ExtServices on T+"); 62 return skipAndCancelBackgroundJob(params, /* skipReason=*/ 0, /* doRecord=*/ false); 63 } 64 65 AdServicesJobServiceLogger.getInstance().recordOnStartJob(params.getJobId()); 66 67 if (FlagsFactory.getFlags().getAsyncRegistrationJobQueueKillSwitch()) { 68 LoggerFactory.getMeasurementLogger().e("AsyncRegistrationQueueJobService is disabled"); 69 return skipAndCancelBackgroundJob( 70 params, 71 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 72 /* doRecord=*/ true); 73 } 74 75 Instant jobStartTime = Clock.systemUTC().instant(); 76 LoggerFactory.getMeasurementLogger() 77 .d( 78 "AsyncRegistrationQueueJobService.onStartJob " + "at %s", 79 jobStartTime.toString()); 80 81 mExecutorFuture = 82 AdServicesExecutors.getBlockingExecutor() 83 .submit( 84 () -> { 85 Trace.beginSection( 86 "AsyncRegistrationQueueJobService#onStartJob#executor"); 87 cancelDeprecatedAsyncRegistrationJob( 88 AsyncRegistrationQueueJobService.this); 89 ProcessingResult result = processAsyncRecords(); 90 LoggerFactory.getMeasurementLogger() 91 .d( 92 "AsyncRegistrationQueueJobService finished" 93 + " processing [%s]", 94 result); 95 96 final boolean shouldRetry = 97 !ProcessingResult.SUCCESS_ALL_RECORDS_PROCESSED.equals( 98 result); 99 final boolean isSuccessful = 100 !ProcessingResult.THREAD_INTERRUPTED.equals(result); 101 AdServicesJobServiceLogger.getInstance() 102 .recordJobFinished( 103 params.getJobId(), isSuccessful, shouldRetry); 104 105 switch (result) { 106 case SUCCESS_ALL_RECORDS_PROCESSED: 107 // Force scheduling to avoid concurrency issue 108 scheduleIfNeeded(this, /* forceSchedule */ true); 109 break; 110 case SUCCESS_WITH_PENDING_RECORDS: 111 scheduleImmediately( 112 AsyncRegistrationQueueJobService.this); 113 break; 114 case THREAD_INTERRUPTED: 115 default: 116 // Reschedule with back-off criteria specified when it 117 // was 118 // scheduled 119 jobFinished(params, /* wantsReschedule= */ true); 120 } 121 DebugReportingJobService.scheduleIfNeeded( 122 getApplicationContext(), /*force schedule*/ false); 123 Trace.endSection(); 124 }); 125 return true; 126 } 127 128 @VisibleForTesting processAsyncRecords()129 ProcessingResult processAsyncRecords() { 130 return JobLockHolder.getInstance(ASYNC_REGISTRATION_PROCESSING) 131 .callWithLock( 132 "AsyncRegistrationQueueJobService", 133 () -> 134 AsyncRegistrationQueueRunner.getInstance() 135 .runAsyncRegistrationQueueWorker(), 136 // Another thread is already processingasync registrations. 137 ProcessingResult.SUCCESS_ALL_RECORDS_PROCESSED); 138 } 139 140 @Override onStopJob(JobParameters params)141 public boolean onStopJob(JobParameters params) { 142 LoggerFactory.getMeasurementLogger().d("AsyncRegistrationQueueJobService.onStopJob"); 143 boolean shouldRetry = true; 144 if (mExecutorFuture != null) { 145 shouldRetry = mExecutorFuture.cancel(/* mayInterruptIfRunning */ true); 146 } 147 AdServicesJobServiceLogger.getInstance() 148 .recordOnStopJob(params, params.getJobId(), shouldRetry); 149 return shouldRetry; 150 } 151 152 @VisibleForTesting schedule(JobScheduler jobScheduler, JobInfo job)153 protected static void schedule(JobScheduler jobScheduler, JobInfo job) { 154 jobScheduler.schedule(job); 155 } 156 157 // TODO(b/311183933): Remove passed in Context from static method. 158 @SuppressWarnings("AvoidStaticContext") 159 @VisibleForTesting buildJobInfo(Context context, Flags flags)160 static JobInfo buildJobInfo(Context context, Flags flags) { 161 return new JobInfo.Builder( 162 MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, 163 new ComponentName(context, AsyncRegistrationQueueJobService.class)) 164 .addTriggerContentUri( 165 new JobInfo.TriggerContentUri( 166 AsyncRegistrationContentProvider.getTriggerUri(), 167 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)) 168 .setTriggerContentUpdateDelay( 169 flags.getMeasurementAsyncRegistrationJobTriggerMinDelayMs()) 170 .setTriggerContentMaxDelay( 171 flags.getMeasurementAsyncRegistrationJobTriggerMaxDelayMs()) 172 .setRequiredNetworkType( 173 flags.getMeasurementAsyncRegistrationQueueJobRequiredNetworkType()) 174 // Can't call addTriggerContentUri() on a persisted job 175 .setPersisted(flags.getMeasurementAsyncRegistrationQueueJobPersisted()) 176 .build(); 177 } 178 179 /** 180 * Schedule Async Registration Queue Job Service if it is not already scheduled 181 * 182 * @param context the context 183 * @param forceSchedule flag to indicate whether to force rescheduling the job. 184 */ 185 // TODO(b/311183933): Remove passed in Context from static method. 186 @SuppressWarnings("AvoidStaticContext") scheduleIfNeeded(Context context, boolean forceSchedule)187 public static void scheduleIfNeeded(Context context, boolean forceSchedule) { 188 Flags flags = FlagsFactory.getFlags(); 189 if (flags.getAsyncRegistrationJobQueueKillSwitch()) { 190 LoggerFactory.getMeasurementLogger() 191 .e("AsyncRegistrationQueueJobService is disabled, skip scheduling"); 192 return; 193 } 194 195 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 196 if (jobScheduler == null) { 197 LoggerFactory.getMeasurementLogger().e("JobScheduler not found"); 198 return; 199 } 200 201 final JobInfo scheduledJobInfo = 202 jobScheduler.getPendingJob(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID); 203 // Schedule if it hasn't been scheduled already or force rescheduling 204 JobInfo jobInfo = buildJobInfo(context, flags); 205 if (forceSchedule || !jobInfo.equals(scheduledJobInfo)) { 206 schedule(jobScheduler, jobInfo); 207 LoggerFactory.getMeasurementLogger().d("Scheduled AsyncRegistrationQueueJobService"); 208 } else { 209 LoggerFactory.getMeasurementLogger() 210 .d("AsyncRegistrationQueueJobService already scheduled, skipping reschedule"); 211 } 212 } 213 214 /** 215 * {@link AsyncRegistrationQueueJobService} used to be periodic. We have this method to cancel 216 * the legacy job. 217 * 218 * @param context application context 219 */ 220 // TODO(b/311183933): Remove passed in Context from static method. 221 @SuppressWarnings("AvoidStaticContext") cancelDeprecatedAsyncRegistrationJob(Context context)222 private static void cancelDeprecatedAsyncRegistrationJob(Context context) { 223 JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 224 int deprecatedAsyncRegJobId = 225 AdServicesJobInfo.DEPRECATED_ASYNC_REGISTRATION_QUEUE_JOB.getJobId(); 226 if (jobScheduler != null && jobScheduler.getPendingJob(deprecatedAsyncRegJobId) != null) { 227 LoggerFactory.getMeasurementLogger() 228 .d( 229 "Cancelling the deprecated async registration job; job_id=%d", 230 deprecatedAsyncRegJobId); 231 jobScheduler.cancel(deprecatedAsyncRegJobId); 232 } 233 } 234 235 @VisibleForTesting scheduleImmediately(Context context)236 void scheduleImmediately(Context context) { 237 Flags flags = FlagsFactory.getFlags(); 238 if (flags.getAsyncRegistrationJobQueueKillSwitch()) { 239 LoggerFactory.getMeasurementLogger() 240 .e("AsyncRegistrationQueueJobService is disabled, skip scheduling"); 241 return; 242 } 243 244 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 245 if (jobScheduler == null) { 246 LoggerFactory.getMeasurementLogger().e("JobScheduler not found"); 247 return; 248 } 249 250 final JobInfo job = 251 new JobInfo.Builder( 252 MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, 253 new ComponentName(context, AsyncRegistrationQueueJobService.class)) 254 .setRequiredNetworkType( 255 flags.getMeasurementAsyncRegistrationQueueJobRequiredNetworkType()) 256 .build(); 257 258 schedule(jobScheduler, job); 259 LoggerFactory.getMeasurementLogger() 260 .d("AsyncRegistrationQueueJobService scheduled to run immediately"); 261 } 262 skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)263 private boolean skipAndCancelBackgroundJob( 264 final JobParameters params, int skipReason, boolean doRecord) { 265 final JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 266 if (jobScheduler != null) { 267 jobScheduler.cancel(params.getJobId()); 268 } 269 270 if (doRecord) { 271 AdServicesJobServiceLogger.getInstance() 272 .recordJobSkipped(params.getJobId(), skipReason); 273 } 274 275 // Tell the JobScheduler that the job is done and does not need to be rescheduled 276 jobFinished(params, false); 277 278 // Returning false to reschedule this job. 279 return false; 280 } 281 282 @VisibleForTesting getFutureForTesting()283 Future getFutureForTesting() { 284 return mExecutorFuture; 285 } 286 } 287