1 /* 2 * Copyright (C) 2024 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_EXTSERVICES_JOB_ON_TPLUS; 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.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED; 22 import static com.android.adservices.spe.AdServicesJobInfo.SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB; 23 24 import android.adservices.customaudience.ScheduleCustomAudienceUpdateRequest; 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.spe.AdServicesJobServiceLogger; 44 import com.android.internal.annotations.VisibleForTesting; 45 46 import com.google.common.util.concurrent.FutureCallback; 47 48 /** 49 * A periodic job that serves as scheduler for handling {@link ScheduleCustomAudienceUpdateRequest}. 50 * When the job is run, it's {@link ScheduleCustomAudienceUpdateWorker} is triggered which initiates 51 * update for pending delayed events. 52 */ 53 @RequiresApi(Build.VERSION_CODES.S) 54 public class ScheduleCustomAudienceUpdateJobService extends JobService { 55 56 private static final int SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID = 57 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB.getJobId(); 58 59 @Override onStartJob(JobParameters params)60 public boolean onStartJob(JobParameters params) { 61 62 // If job is not supposed to be running, cancel itself. 63 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 64 LogUtil.d( 65 "Disabling ScheduleCustomAudienceUpdate job because it's running in ExtServices" 66 + " on T+"); 67 return skipAndCancelBackgroundJob( 68 params, 69 /* skipReason=*/ AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS, 70 /* doRecord=*/ false); 71 } 72 73 LoggerFactory.getFledgeLogger().d("ScheduleCustomAudienceUpdateJobService.onStartJob"); 74 75 AdServicesJobServiceLogger.getInstance() 76 .recordOnStartJob(SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID); 77 78 if (!FlagsFactory.getFlags().getFledgeScheduleCustomAudienceUpdateEnabled()) { 79 LoggerFactory.getFledgeLogger() 80 .d( 81 "FLEDGE Schedule Custom Audience Update API is disabled ; skipping and" 82 + " cancelling job"); 83 return skipAndCancelBackgroundJob( 84 params, 85 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 86 /* doRecord=*/ true); 87 } 88 89 // Skip the execution and cancel the job if user consent is revoked. 90 // Use the per-API consent with GA UX. 91 if (!ConsentManager.getInstance().getConsent(AdServicesApiType.FLEDGE).isGiven()) { 92 LoggerFactory.getFledgeLogger() 93 .d("User Consent is revoked ; skipping and cancelling job"); 94 return skipAndCancelBackgroundJob( 95 params, 96 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED, 97 /* doRecord=*/ true); 98 } 99 100 ScheduleCustomAudienceUpdateWorker updateWorker = 101 ScheduleCustomAudienceUpdateWorker.getInstance(this); 102 updateWorker 103 .updateCustomAudience() 104 .addCallback( 105 new FutureCallback<Void>() { 106 @Override 107 public void onSuccess(Void result) { 108 LoggerFactory.getFledgeLogger() 109 .d("Schedule Custom Audience Update job completed"); 110 111 boolean shouldRetry = false; 112 AdServicesJobServiceLogger.getInstance() 113 .recordJobFinished( 114 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID, 115 /* isSuccessful= */ true, 116 shouldRetry); 117 118 jobFinished(params, shouldRetry); 119 } 120 121 @Override 122 public void onFailure(Throwable t) { 123 boolean shouldRetry = false; 124 LoggerFactory.getFledgeLogger() 125 .e(t, "Schedule Custom Audience Update job worker failed"); 126 AdServicesJobServiceLogger.getInstance() 127 .recordJobFinished( 128 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID, 129 /* isSuccessful= */ false, 130 shouldRetry); 131 132 jobFinished(params, shouldRetry); 133 } 134 }, 135 AdServicesExecutors.getLightWeightExecutor()); 136 return true; 137 } 138 139 @Override onStopJob(JobParameters params)140 public boolean onStopJob(JobParameters params) { 141 LoggerFactory.getFledgeLogger().d("ScheduleCustomAudienceUpdateJobService.onStopJob"); 142 ScheduleCustomAudienceUpdateWorker.getInstance(this).stopWork(); 143 144 boolean shouldRetry = true; 145 AdServicesJobServiceLogger.getInstance() 146 .recordOnStopJob( 147 params, SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID, shouldRetry); 148 149 return shouldRetry; 150 } 151 152 /** 153 * Attempts to schedule the update Custom Audience job as a singleton job if it is not already 154 * scheduled. 155 */ scheduleIfNeeded(Context context, Flags flags, boolean forceSchedule)156 public static void scheduleIfNeeded(Context context, Flags flags, boolean forceSchedule) { 157 LoggerFactory.getFledgeLogger() 158 .v( 159 "Attempting to schedule job:%s if needed", 160 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID); 161 162 if (!flags.getFledgeScheduleCustomAudienceUpdateEnabled()) { 163 LoggerFactory.getFledgeLogger() 164 .d( 165 "FLEDGE Schedule Custom Audience Update API is disabled ; skipping and" 166 + " cancelling job"); 167 return; 168 } 169 170 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 171 if ((jobScheduler.getPendingJob(SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID) == null) 172 || forceSchedule) { 173 schedule(context, flags); 174 } else { 175 LoggerFactory.getFledgeLogger() 176 .v( 177 "FLEDGE Schedule Custom Audience Update job already scheduled, skipping" 178 + " reschedule"); 179 } 180 // TODO(b/267651517) Jobs should be rescheduled if the job-params get updated 181 } 182 183 /** 184 * Actually schedules the Update Custom Audience job as a singleton job. 185 * 186 * <p>Split out from {@link #scheduleIfNeeded(Context, Flags, boolean)} for mockable testing 187 */ 188 @VisibleForTesting schedule(Context context, Flags flags)189 protected static void schedule(Context context, Flags flags) { 190 if (!flags.getFledgeScheduleCustomAudienceUpdateEnabled()) { 191 LoggerFactory.getFledgeLogger() 192 .v( 193 "FLEDGE Schedule Custom Audience Update API is disabled;" 194 + " skipping schedule"); 195 return; 196 } 197 198 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 199 final JobInfo job = 200 new JobInfo.Builder( 201 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID, 202 new ComponentName( 203 context, ScheduleCustomAudienceUpdateJobService.class)) 204 .setRequiresBatteryNotLow(true) 205 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) 206 .setPeriodic( 207 flags.getFledgeScheduleCustomAudienceUpdateJobPeriodMs(), 208 flags.getFledgeScheduleCustomAudienceUpdateJobFlexMs()) 209 .setPersisted(true) 210 .build(); 211 jobScheduler.schedule(job); 212 } 213 skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)214 private boolean skipAndCancelBackgroundJob( 215 final JobParameters params, int skipReason, boolean doRecord) { 216 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 217 if (jobScheduler != null) { 218 jobScheduler.cancel(SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID); 219 } 220 221 if (doRecord) { 222 AdServicesJobServiceLogger.getInstance() 223 .recordJobSkipped( 224 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID, skipReason); 225 } 226 227 jobFinished(params, false); 228 return false; 229 } 230 } 231