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; 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.spe.AdServicesJobInfo.MAINTENANCE_JOB; 21 22 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 23 24 import android.annotation.NonNull; 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.concurrency.AdServicesExecutors; 37 import com.android.adservices.service.common.FledgeMaintenanceTasksWorker; 38 import com.android.adservices.service.common.compat.ServiceCompatUtils; 39 import com.android.adservices.service.signals.SignalsMaintenanceTasksWorker; 40 import com.android.adservices.service.topics.TopicsWorker; 41 import com.android.adservices.spe.AdServicesJobServiceLogger; 42 import com.android.internal.annotations.VisibleForTesting; 43 44 import com.google.common.util.concurrent.FutureCallback; 45 import com.google.common.util.concurrent.Futures; 46 import com.google.common.util.concurrent.ListenableFuture; 47 48 import java.util.List; 49 import java.util.Objects; 50 51 /** Maintenance job to clean up. */ 52 @RequiresApi(Build.VERSION_CODES.S) 53 public final class MaintenanceJobService extends JobService { 54 private static final int MAINTENANCE_JOB_ID = MAINTENANCE_JOB.getJobId(); 55 56 private FledgeMaintenanceTasksWorker mFledgeMaintenanceTasksWorker; 57 58 private SignalsMaintenanceTasksWorker mSignalsMaintenanceTasksWorker; 59 60 /** Injects a {@link FledgeMaintenanceTasksWorker to be used during testing} */ 61 @VisibleForTesting injectFledgeMaintenanceTasksWorker( @onNull FledgeMaintenanceTasksWorker fledgeMaintenanceTasksWorker)62 public void injectFledgeMaintenanceTasksWorker( 63 @NonNull FledgeMaintenanceTasksWorker fledgeMaintenanceTasksWorker) { 64 mFledgeMaintenanceTasksWorker = fledgeMaintenanceTasksWorker; 65 } 66 67 /** Injects a {@link SignalsMaintenanceTasksWorker to be used during testing} */ 68 @VisibleForTesting injectSignalsMaintenanceTasksWorker( @onNull SignalsMaintenanceTasksWorker signalsMaintenanceTasksWorker)69 public void injectSignalsMaintenanceTasksWorker( 70 @NonNull SignalsMaintenanceTasksWorker signalsMaintenanceTasksWorker) { 71 mSignalsMaintenanceTasksWorker = signalsMaintenanceTasksWorker; 72 } 73 74 @Override onStartJob(JobParameters params)75 public boolean onStartJob(JobParameters params) { 76 // Always ensure that the first thing this job does is check if it should be running, and 77 // cancel itself if it's not supposed to be. 78 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 79 LogUtil.d( 80 "Disabling MaintenanceJobService job because it's running in ExtServices on" 81 + " T+"); 82 return skipAndCancelBackgroundJob(params, /* skipReason= */ 0, /* doRecord= */ false); 83 } 84 85 Flags flags = FlagsFactory.getFlags(); 86 87 LogUtil.d("MaintenanceJobService.onStartJob"); 88 AdServicesJobServiceLogger.getInstance().recordOnStartJob(MAINTENANCE_JOB_ID); 89 90 if (flags.getTopicsKillSwitch() 91 && flags.getFledgeSelectAdsKillSwitch() 92 && (!flags.getProtectedSignalsCleanupEnabled() || flags.getGlobalKillSwitch())) { 93 LogUtil.e( 94 "All maintenance jobs are disabled, skipping and cancelling" 95 + " MaintenanceJobService"); 96 return skipAndCancelBackgroundJob( 97 params, 98 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 99 /* doRecord= */ true); 100 } 101 102 ListenableFuture<Void> appReconciliationFuture; 103 if (flags.getTopicsKillSwitch()) { 104 LogUtil.d("Topics API is disabled, skipping Topics Job"); 105 appReconciliationFuture = Futures.immediateFuture(null); 106 } else { 107 appReconciliationFuture = 108 Futures.submit( 109 () -> TopicsWorker.getInstance().reconcileApplicationUpdate(this), 110 AdServicesExecutors.getBackgroundExecutor()); 111 } 112 113 ListenableFuture<Void> fledgeMaintenanceTasksFuture; 114 if (flags.getFledgeSelectAdsKillSwitch()) { 115 LogUtil.d("Ad Selection API is disabled, skipping Ad Selection Maintenance Job"); 116 fledgeMaintenanceTasksFuture = Futures.immediateFuture(null); 117 } else { 118 fledgeMaintenanceTasksFuture = 119 Futures.submit( 120 this::doAdSelectionDataMaintenanceTasks, 121 AdServicesExecutors.getBackgroundExecutor()); 122 } 123 124 ListenableFuture<Void> protectedSignalsMaintenanceTasksFuture; 125 if (!flags.getProtectedSignalsCleanupEnabled()) { 126 LogUtil.d("Signals cleanup is disabled, skipping maintenance job"); 127 protectedSignalsMaintenanceTasksFuture = Futures.immediateFuture(null); 128 } else { 129 protectedSignalsMaintenanceTasksFuture = 130 Futures.submit( 131 this::doProtectedSignalsDataMaintenanceTasks, 132 AdServicesExecutors.getBackgroundExecutor()); 133 } 134 135 ListenableFuture<List<Void>> futuresList = 136 Futures.allAsList( 137 fledgeMaintenanceTasksFuture, 138 protectedSignalsMaintenanceTasksFuture, 139 appReconciliationFuture); 140 141 Futures.addCallback( 142 futuresList, 143 new FutureCallback<List<Void>>() { 144 @Override 145 public void onSuccess(List<Void> result) { 146 boolean shouldRetry = false; 147 AdServicesJobServiceLogger.getInstance() 148 .recordJobFinished( 149 MAINTENANCE_JOB_ID, /* isSuccessful= */ true, shouldRetry); 150 151 LogUtil.d("PP API jobs are done!"); 152 jobFinished(params, shouldRetry); 153 } 154 155 @Override 156 public void onFailure(Throwable t) { 157 boolean shouldRetry = false; 158 AdServicesJobServiceLogger.getInstance() 159 .recordJobFinished( 160 MAINTENANCE_JOB_ID, /* isSuccessful= */ false, shouldRetry); 161 162 LogUtil.e( 163 t, "Failed to handle MaintenanceJobService: " + params.getJobId()); 164 jobFinished(params, shouldRetry); 165 } 166 }, 167 directExecutor()); 168 return true; 169 } 170 171 @Override onStopJob(JobParameters params)172 public boolean onStopJob(JobParameters params) { 173 LogUtil.d("MaintenanceJobService.onStopJob"); 174 175 // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the 176 // execution is completed or not to avoid executing the task twice. 177 boolean shouldRetry = false; 178 179 AdServicesJobServiceLogger.getInstance() 180 .recordOnStopJob(params, MAINTENANCE_JOB_ID, shouldRetry); 181 return shouldRetry; 182 } 183 184 @VisibleForTesting schedule( Context context, @NonNull JobScheduler jobScheduler, long maintenanceJobPeriodMs, long maintenanceJobFlexMs)185 static void schedule( 186 Context context, 187 @NonNull JobScheduler jobScheduler, 188 long maintenanceJobPeriodMs, 189 long maintenanceJobFlexMs) { 190 final JobInfo job = 191 new JobInfo.Builder( 192 MAINTENANCE_JOB_ID, 193 new ComponentName(context, MaintenanceJobService.class)) 194 .setRequiresCharging(true) 195 .setPersisted(true) 196 .setPeriodic(maintenanceJobPeriodMs, maintenanceJobFlexMs) 197 .build(); 198 199 jobScheduler.schedule(job); 200 LogUtil.d("Scheduling maintenance job ..."); 201 } 202 203 /** 204 * Schedule Maintenance Job Service if needed: there is no scheduled job with same job 205 * parameters. 206 * 207 * @param context the context 208 * @param forceSchedule a flag to indicate whether to force rescheduling the job. 209 * @return a {@code boolean} to indicate if the service job is actually scheduled. 210 */ scheduleIfNeeded(Context context, boolean forceSchedule)211 public static boolean scheduleIfNeeded(Context context, boolean forceSchedule) { 212 Flags flags = FlagsFactory.getFlags(); 213 214 if (flags.getTopicsKillSwitch() && flags.getFledgeSelectAdsKillSwitch()) { 215 LogUtil.e( 216 "Both Topics and Select Ads are disabled, skipping scheduling" 217 + " MaintenanceJobService"); 218 return false; 219 } 220 221 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 222 if (jobScheduler == null) { 223 LogUtil.e("Cannot fetch Job Scheduler!"); 224 return false; 225 } 226 227 long flagsMaintenanceJobPeriodMs = flags.getMaintenanceJobPeriodMs(); 228 long flagsMaintenanceJobFlexMs = flags.getMaintenanceJobFlexMs(); 229 230 JobInfo job = jobScheduler.getPendingJob(MAINTENANCE_JOB_ID); 231 // Skip to reschedule the job if there is same scheduled job with same parameters. 232 if (job != null && !forceSchedule) { 233 long maintenanceJobPeriodMs = job.getIntervalMillis(); 234 long maintenanceJobFlexMs = job.getFlexMillis(); 235 236 if (flagsMaintenanceJobPeriodMs == maintenanceJobPeriodMs 237 && flagsMaintenanceJobFlexMs == maintenanceJobFlexMs) { 238 LogUtil.d( 239 "Maintenance Job Service has been scheduled with same parameters, skip" 240 + " rescheduling!"); 241 return false; 242 } 243 } 244 245 schedule(context, jobScheduler, flagsMaintenanceJobPeriodMs, flagsMaintenanceJobFlexMs); 246 return true; 247 } 248 skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)249 private boolean skipAndCancelBackgroundJob( 250 final JobParameters params, int skipReason, boolean doRecord) { 251 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 252 if (jobScheduler != null) { 253 jobScheduler.cancel(MAINTENANCE_JOB_ID); 254 } 255 256 if (doRecord) { 257 AdServicesJobServiceLogger.getInstance() 258 .recordJobSkipped(MAINTENANCE_JOB_ID, skipReason); 259 } 260 261 // Tell the JobScheduler that the job has completed and does not need to be 262 // rescheduled. 263 jobFinished(params, false); 264 265 // Returning false means that this job has completed its work. 266 return false; 267 } 268 getFledgeMaintenanceTasksWorker()269 private FledgeMaintenanceTasksWorker getFledgeMaintenanceTasksWorker() { 270 if (!Objects.isNull(mFledgeMaintenanceTasksWorker)) { 271 return mFledgeMaintenanceTasksWorker; 272 } 273 mFledgeMaintenanceTasksWorker = FledgeMaintenanceTasksWorker.create(this); 274 return mFledgeMaintenanceTasksWorker; 275 } 276 getSignalsMaintenanceTasksWorker()277 private SignalsMaintenanceTasksWorker getSignalsMaintenanceTasksWorker() { 278 if (!Objects.isNull(mSignalsMaintenanceTasksWorker)) { 279 return mSignalsMaintenanceTasksWorker; 280 } 281 mSignalsMaintenanceTasksWorker = SignalsMaintenanceTasksWorker.create(this); 282 return mSignalsMaintenanceTasksWorker; 283 } 284 doAdSelectionDataMaintenanceTasks()285 private void doAdSelectionDataMaintenanceTasks() { 286 LogUtil.v("Performing Ad Selection maintenance tasks"); 287 getFledgeMaintenanceTasksWorker().clearExpiredAdSelectionData(); 288 getFledgeMaintenanceTasksWorker() 289 .clearInvalidFrequencyCapHistogramData(this.getPackageManager()); 290 getFledgeMaintenanceTasksWorker().clearExpiredKAnonMessageEntities(); 291 } 292 doProtectedSignalsDataMaintenanceTasks()293 private void doProtectedSignalsDataMaintenanceTasks() { 294 LogUtil.v("Performing protected signals maintenance tasks"); 295 getSignalsMaintenanceTasksWorker().clearInvalidProtectedSignalsData(); 296 } 297 } 298