1 /* 2 * Copyright (C) 2022 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.attribution; 18 19 import static com.android.adservices.service.measurement.util.JobLockHolder.Type.ATTRIBUTION_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_ATTRIBUTION_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.data.measurement.DatastoreManagerFactory; 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.Trigger; 38 import com.android.adservices.service.measurement.attribution.AttributionJobHandler.ProcessingResult; 39 import com.android.adservices.service.measurement.reporting.DebugReportApi; 40 import com.android.adservices.service.measurement.reporting.DebugReportingJobService; 41 import com.android.adservices.service.measurement.reporting.ImmediateAggregateReportingJobService; 42 import com.android.adservices.service.measurement.reporting.ReportingJobService; 43 import com.android.adservices.service.measurement.util.JobLockHolder; 44 import com.android.adservices.spe.AdServicesJobServiceLogger; 45 import com.android.internal.annotations.VisibleForTesting; 46 47 import com.google.common.util.concurrent.ListeningExecutorService; 48 49 import java.util.concurrent.Future; 50 51 /** 52 * Service for scheduling attribution jobs. The actual job execution logic is part of {@link 53 * AttributionJobHandler}. 54 */ 55 public class AttributionJobService extends JobService { 56 private static final int MEASUREMENT_ATTRIBUTION_JOB_ID = 57 MEASUREMENT_ATTRIBUTION_JOB.getJobId(); 58 private static final ListeningExecutorService sBackgroundExecutor = 59 AdServicesExecutors.getBackgroundExecutor(); 60 61 private Future mExecutorFuture; 62 63 @Override onCreate()64 public void onCreate() { 65 LogUtil.d("AttributionJobService.onCreate"); 66 super.onCreate(); 67 } 68 69 @Override onStartJob(JobParameters params)70 public boolean onStartJob(JobParameters params) { 71 // Always ensure that the first thing this job does is check if it should be running, and 72 // cancel itself if it's not supposed to be. 73 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 74 LogUtil.d( 75 "Disabling AttributionJobService job because it's running in ExtServices on" 76 + " T+"); 77 return skipAndCancelBackgroundJob(params, /* skipReason=*/ 0, /* doRecord=*/ false); 78 } 79 80 AdServicesJobServiceLogger.getInstance().recordOnStartJob(MEASUREMENT_ATTRIBUTION_JOB_ID); 81 82 if (FlagsFactory.getFlags().getMeasurementJobAttributionKillSwitch()) { 83 LoggerFactory.getMeasurementLogger().e("AttributionJobService is disabled"); 84 return skipAndCancelBackgroundJob( 85 params, 86 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 87 /* doRecord=*/ true); 88 } 89 90 LoggerFactory.getMeasurementLogger().d("AttributionJobService.onStartJob"); 91 mExecutorFuture = 92 sBackgroundExecutor.submit( 93 () -> { 94 ProcessingResult result = acquireLockAndProcessPendingAttributions(); 95 LoggerFactory.getMeasurementLogger() 96 .d("AttributionJobService finished processing [%s]", result); 97 98 final boolean shouldRetry = 99 !ProcessingResult.SUCCESS_ALL_RECORDS_PROCESSED.equals(result); 100 final boolean isSuccessful = !ProcessingResult.FAILURE.equals(result); 101 AdServicesJobServiceLogger.getInstance() 102 .recordJobFinished( 103 MEASUREMENT_ATTRIBUTION_JOB_ID, 104 isSuccessful, 105 shouldRetry); 106 107 switch (result) { 108 case SUCCESS_ALL_RECORDS_PROCESSED: 109 // Force scheduling to avoid concurrency issue 110 scheduleIfNeeded(this, /* forceSchedule */ true); 111 break; 112 case SUCCESS_WITH_PENDING_RECORDS: 113 scheduleImmediately(AttributionJobService.this); 114 break; 115 case FAILURE: 116 default: 117 // Reschedule with back-off criteria specified when it was 118 // scheduled 119 jobFinished(params, /* wantsReschedule= */ true); 120 } 121 122 DebugReportingJobService.scheduleIfNeeded( 123 getApplicationContext(), /* forceSchedule */ false); 124 125 // TODO(b/342687685): fold this service into ReportingJobService 126 ImmediateAggregateReportingJobService.scheduleIfNeeded( 127 getApplicationContext(), /* forceSchedule */ false); 128 129 ReportingJobService.scheduleIfNeeded( 130 getApplicationContext(), /* forceSchedule */ false); 131 }); 132 return true; 133 } 134 135 @VisibleForTesting acquireLockAndProcessPendingAttributions()136 ProcessingResult acquireLockAndProcessPendingAttributions() { 137 final JobLockHolder lock = JobLockHolder.getInstance(ATTRIBUTION_PROCESSING); 138 if (lock.tryLock()) { 139 try { 140 return processPendingAttributions(); 141 } finally { 142 lock.unlock(); 143 } 144 } 145 LoggerFactory.getMeasurementLogger().d("AttributionJobService did not acquire the lock"); 146 // Another thread is already processing attribution. Returning success to not reschedule. 147 return ProcessingResult.SUCCESS_ALL_RECORDS_PROCESSED; 148 } 149 150 @VisibleForTesting processPendingAttributions()151 ProcessingResult processPendingAttributions() { 152 return new AttributionJobHandler( 153 DatastoreManagerFactory.getDatastoreManager(getApplicationContext()), 154 new DebugReportApi(getApplicationContext(), FlagsFactory.getFlags())) 155 .performPendingAttributions(); 156 } 157 158 @Override onStopJob(JobParameters params)159 public boolean onStopJob(JobParameters params) { 160 LoggerFactory.getMeasurementLogger().d("AttributionJobService.onStopJob"); 161 boolean shouldRetry = true; 162 if (mExecutorFuture != null) { 163 shouldRetry = mExecutorFuture.cancel(/* mayInterruptIfRunning */ true); 164 } 165 AdServicesJobServiceLogger.getInstance() 166 .recordOnStopJob(params, MEASUREMENT_ATTRIBUTION_JOB_ID, shouldRetry); 167 return shouldRetry; 168 } 169 170 /** Schedules {@link AttributionJobService} to observer {@link Trigger} content URI change. */ 171 @VisibleForTesting schedule(JobScheduler jobScheduler, JobInfo jobInfo)172 static void schedule(JobScheduler jobScheduler, JobInfo jobInfo) { 173 jobScheduler.schedule(jobInfo); 174 } 175 buildJobInfo(Context context, Flags flags)176 private static JobInfo buildJobInfo(Context context, Flags flags) { 177 return new JobInfo.Builder( 178 MEASUREMENT_ATTRIBUTION_JOB_ID, 179 new ComponentName(context, AttributionJobService.class)) 180 .addTriggerContentUri( 181 new JobInfo.TriggerContentUri( 182 TriggerContentProvider.TRIGGER_URI, 183 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)) 184 .setTriggerContentUpdateDelay(flags.getMeasurementAttributionJobTriggeringDelayMs()) 185 // Can't call addTriggerContentUri() on a persisted job 186 .setPersisted(flags.getMeasurementAttributionJobPersisted()) 187 .build(); 188 } 189 190 /** 191 * Schedule Attribution Job if it is not already scheduled 192 * 193 * @param context the context 194 * @param forceSchedule flag to indicate whether to force rescheduling the job. 195 */ scheduleIfNeeded(Context context, boolean forceSchedule)196 public static void scheduleIfNeeded(Context context, boolean forceSchedule) { 197 Flags flags = FlagsFactory.getFlags(); 198 if (flags.getMeasurementJobAttributionKillSwitch()) { 199 LoggerFactory.getMeasurementLogger() 200 .e("AttributionJobService is disabled, skip scheduling"); 201 return; 202 } 203 204 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 205 if (jobScheduler == null) { 206 LoggerFactory.getMeasurementLogger().e("JobScheduler not found"); 207 return; 208 } 209 210 final JobInfo scheduledJob = jobScheduler.getPendingJob(MEASUREMENT_ATTRIBUTION_JOB_ID); 211 // Schedule if it hasn't been scheduled already or force rescheduling 212 final JobInfo job = buildJobInfo(context, flags); 213 if (forceSchedule || !job.equals(scheduledJob)) { 214 schedule(jobScheduler, job); 215 LoggerFactory.getMeasurementLogger().d("Scheduled AttributionJobService"); 216 } else { 217 LoggerFactory.getMeasurementLogger() 218 .d("AttributionJobService already scheduled, skipping reschedule"); 219 } 220 } 221 222 @VisibleForTesting scheduleImmediately(Context context)223 void scheduleImmediately(Context context) { 224 if (FlagsFactory.getFlags().getMeasurementJobAttributionKillSwitch()) { 225 LoggerFactory.getMeasurementLogger() 226 .e("AttributionJobService is disabled, skip scheduling"); 227 return; 228 } 229 230 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 231 if (jobScheduler == null) { 232 LoggerFactory.getMeasurementLogger().e("JobScheduler not found"); 233 return; 234 } 235 236 final JobInfo job = 237 new JobInfo.Builder( 238 MEASUREMENT_ATTRIBUTION_JOB_ID, 239 new ComponentName(context, AttributionJobService.class)) 240 .build(); 241 242 schedule(jobScheduler, job); 243 LoggerFactory.getMeasurementLogger() 244 .d("AttributionJobService scheduled to run immediately"); 245 } 246 skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)247 private boolean skipAndCancelBackgroundJob( 248 final JobParameters params, int skipReason, boolean doRecord) { 249 final JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 250 if (jobScheduler != null) { 251 jobScheduler.cancel(MEASUREMENT_ATTRIBUTION_JOB_ID); 252 } 253 254 if (doRecord) { 255 AdServicesJobServiceLogger.getInstance() 256 .recordJobSkipped(MEASUREMENT_ATTRIBUTION_JOB_ID, skipReason); 257 } 258 259 // Tell the JobScheduler that the job has completed and does not need to be rescheduled. 260 jobFinished(params, false); 261 262 // Returning false means that this job has completed its work. 263 return false; 264 } 265 266 @VisibleForTesting getFutureForTesting()267 Future getFutureForTesting() { 268 return mExecutorFuture; 269 } 270 } 271