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.customaudience; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED; 21 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED; 22 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL; 23 import static com.android.adservices.spe.AdServicesJobInfo.FLEDGE_BACKGROUND_FETCH_JOB; 24 25 import android.app.job.JobInfo; 26 import android.app.job.JobParameters; 27 import android.app.job.JobScheduler; 28 import android.app.job.JobService; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.os.Build; 32 33 import androidx.annotation.RequiresApi; 34 35 import com.android.adservices.LogUtil; 36 import com.android.adservices.LoggerFactory; 37 import com.android.adservices.concurrency.AdServicesExecutors; 38 import com.android.adservices.service.Flags; 39 import com.android.adservices.service.FlagsFactory; 40 import com.android.adservices.service.common.compat.ServiceCompatUtils; 41 import com.android.adservices.service.consent.AdServicesApiType; 42 import com.android.adservices.service.consent.ConsentManager; 43 import com.android.adservices.shared.common.ApplicationContextSingleton; 44 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode; 45 import com.android.adservices.spe.AdServicesJobServiceLogger; 46 import com.android.internal.annotations.VisibleForTesting; 47 48 import com.google.common.util.concurrent.FutureCallback; 49 50 import java.time.Clock; 51 import java.time.Instant; 52 import java.util.concurrent.ExecutionException; 53 import java.util.concurrent.TimeoutException; 54 55 /** 56 * Background fetch for FLEDGE Custom Audience API, executing periodic garbage collection and custom 57 * audience updates. 58 */ 59 // TODO(b/269798827): Enable for R. 60 @RequiresApi(Build.VERSION_CODES.S) 61 public class BackgroundFetchJobService extends JobService { 62 private static final int FLEDGE_BACKGROUND_FETCH_JOB_ID = 63 FLEDGE_BACKGROUND_FETCH_JOB.getJobId(); 64 65 @Override onStartJob(JobParameters params)66 public boolean onStartJob(JobParameters params) { 67 // Always ensure that the first thing this job does is check if it should be running, and 68 // cancel itself if it's not supposed to be. 69 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 70 LogUtil.d( 71 "Disabling BackgroundFetchJobService job because it's running in ExtServices" 72 + " on T+"); 73 return skipAndCancelBackgroundJob(params, /* skipReason= */ 0, /* doRecord= */ false); 74 } 75 76 LoggerFactory.getFledgeLogger().d("BackgroundFetchJobService.onStartJob"); 77 78 AdServicesJobServiceLogger.getInstance().recordOnStartJob(FLEDGE_BACKGROUND_FETCH_JOB_ID); 79 80 Flags flags = FlagsFactory.getFlags(); 81 if (!flags.getFledgeBackgroundFetchEnabled()) { 82 LoggerFactory.getFledgeLogger() 83 .d("FLEDGE background fetch is disabled; skipping and cancelling job"); 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 if (flags.getFledgeCustomAudienceServiceKillSwitch()) { 91 LoggerFactory.getFledgeLogger() 92 .d("FLEDGE Custom Audience API is disabled ; skipping and cancelling job"); 93 return skipAndCancelBackgroundJob( 94 params, 95 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 96 /* doRecord= */ true); 97 } 98 99 // Skip the execution and cancel the job if user consent is revoked. 100 // Use the per-API consent with GA UX. 101 if (!ConsentManager.getInstance().getConsent(AdServicesApiType.FLEDGE).isGiven()) { 102 LoggerFactory.getFledgeLogger() 103 .d("User Consent is revoked ; skipping and cancelling job"); 104 return skipAndCancelBackgroundJob( 105 params, 106 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED, 107 /* doRecord= */ true); 108 } 109 110 // TODO(b/235841960): Consider using com.android.adservices.service.stats.Clock instead of 111 // Java Clock 112 Instant jobStartTime = Clock.systemUTC().instant(); 113 LoggerFactory.getFledgeLogger() 114 .d("Starting FLEDGE background fetch job at %s", jobStartTime.toString()); 115 116 BackgroundFetchWorker.getInstance(this) 117 .runBackgroundFetch() 118 .addCallback( 119 new FutureCallback<Void>() { 120 // Never manually reschedule the background fetch job, since it is 121 // already scheduled periodically and should try again multiple times 122 // per day 123 @Override 124 public void onSuccess(Void result) { 125 boolean shouldRetry = false; 126 AdServicesJobServiceLogger.getInstance() 127 .recordJobFinished( 128 FLEDGE_BACKGROUND_FETCH_JOB_ID, 129 /* isSuccessful= */ true, 130 shouldRetry); 131 132 jobFinished(params, shouldRetry); 133 } 134 135 @Override 136 public void onFailure(Throwable t) { 137 if (t instanceof InterruptedException) { 138 LoggerFactory.getFledgeLogger() 139 .e( 140 t, 141 "FLEDGE background fetch interrupted while" 142 + " waiting for custom audience updates"); 143 } else if (t instanceof ExecutionException) { 144 LoggerFactory.getFledgeLogger() 145 .e( 146 t, 147 "FLEDGE background fetch failed due to" 148 + " internal error"); 149 } else if (t instanceof TimeoutException) { 150 LoggerFactory.getFledgeLogger() 151 .e(t, "FLEDGE background fetch timeout exceeded"); 152 } else { 153 LoggerFactory.getFledgeLogger() 154 .e( 155 t, 156 "FLEDGE background fetch failed due to" 157 + " unexpected error"); 158 } 159 160 boolean shouldRetry = false; 161 AdServicesJobServiceLogger.getInstance() 162 .recordJobFinished( 163 FLEDGE_BACKGROUND_FETCH_JOB_ID, 164 /* isSuccessful= */ false, 165 shouldRetry); 166 167 jobFinished(params, shouldRetry); 168 } 169 }, 170 AdServicesExecutors.getLightWeightExecutor()); 171 172 // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this 173 // BackgroundFetchJobService will be cancelled for the same job ID. 174 // 175 // Also for a job with Flex Period, it will NOT execute immediately after rescheduling it. 176 // Reschedule it here to let the execution complete and the next cycle will execute with 177 // the BackgroundFetchJob.schedule(). 178 if (flags.getSpeOnBackgroundFetchJobEnabled()) { 179 LoggerFactory.getFledgeLogger() 180 .d("SPE is enabled. Reschedule BackgroundFetchJobService with SPE framework."); 181 BackgroundFetchJob.schedule(flags); 182 } 183 184 return true; 185 } 186 skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)187 private boolean skipAndCancelBackgroundJob( 188 final JobParameters params, int skipReason, boolean doRecord) { 189 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 190 if (jobScheduler != null) { 191 jobScheduler.cancel(FLEDGE_BACKGROUND_FETCH_JOB_ID); 192 } 193 194 if (doRecord) { 195 AdServicesJobServiceLogger.getInstance() 196 .recordJobSkipped(FLEDGE_BACKGROUND_FETCH_JOB_ID, skipReason); 197 } 198 199 jobFinished(params, false); 200 return false; 201 } 202 203 @Override onStopJob(JobParameters params)204 public boolean onStopJob(JobParameters params) { 205 LoggerFactory.getFledgeLogger().d("BackgroundFetchJobService.onStopJob"); 206 BackgroundFetchWorker.getInstance(this).stopWork(); 207 208 boolean shouldRetry = true; 209 210 AdServicesJobServiceLogger.getInstance() 211 .recordOnStopJob(params, FLEDGE_BACKGROUND_FETCH_JOB_ID, shouldRetry); 212 return shouldRetry; 213 } 214 215 /** 216 * Attempts to schedule the FLEDGE Background Fetch as a singleton periodic job if it is not 217 * already scheduled. 218 * 219 * <p>The background fetch primarily updates custom audiences' ads and bidding data. It also 220 * prunes the custom audience database of any expired data. 221 */ 222 @JobSchedulingResultCode scheduleIfNeeded(Flags flags, boolean forceSchedule)223 public static int scheduleIfNeeded(Flags flags, boolean forceSchedule) { 224 Context context = ApplicationContextSingleton.get(); 225 if (!flags.getFledgeBackgroundFetchEnabled()) { 226 LoggerFactory.getFledgeLogger() 227 .v("FLEDGE background fetch is disabled; skipping schedule"); 228 return SCHEDULING_RESULT_CODE_SKIPPED; 229 } 230 231 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 232 233 // Scheduling a job can be expensive, and forcing a schedule could interrupt a job that is 234 // already in progress 235 // TODO(b/221837833): Intelligently decide when to overwrite a scheduled job 236 if ((jobScheduler.getPendingJob(FLEDGE_BACKGROUND_FETCH_JOB_ID) == null) || forceSchedule) { 237 schedule(context, flags); 238 LoggerFactory.getFledgeLogger().d("Scheduled FLEDGE Background Fetch job"); 239 return SCHEDULING_RESULT_CODE_SUCCESSFUL; 240 } else { 241 LoggerFactory.getFledgeLogger() 242 .v("FLEDGE Background Fetch job already scheduled, skipping reschedule"); 243 return SCHEDULING_RESULT_CODE_SKIPPED; 244 } 245 } 246 247 /** 248 * Actually schedules the FLEDGE Background Fetch as a singleton periodic job. 249 * 250 * <p>Split out from {@link #scheduleIfNeeded(Flags, boolean)} for mockable testing without 251 * pesky permissions. 252 */ 253 @VisibleForTesting schedule(Context context, Flags flags)254 protected static void schedule(Context context, Flags flags) { 255 if (!flags.getFledgeBackgroundFetchEnabled()) { 256 LoggerFactory.getFledgeLogger() 257 .v("FLEDGE background fetch is disabled; skipping schedule"); 258 return; 259 } 260 261 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 262 final JobInfo job = 263 new JobInfo.Builder( 264 FLEDGE_BACKGROUND_FETCH_JOB_ID, 265 new ComponentName(context, BackgroundFetchJobService.class)) 266 .setRequiresBatteryNotLow(true) 267 .setRequiresDeviceIdle(true) 268 .setPeriodic( 269 flags.getFledgeBackgroundFetchJobPeriodMs(), 270 flags.getFledgeBackgroundFetchJobFlexMs()) 271 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) 272 .setPersisted(true) 273 .build(); 274 jobScheduler.schedule(job); 275 } 276 } 277