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<Boolean> appReconciliationFuture; 103 if (flags.getTopicsKillSwitch()) { 104 LogUtil.d("Topics API is disabled, skipping Topics Job"); 105 appReconciliationFuture = Futures.immediateFuture(true); 106 } else { 107 appReconciliationFuture = 108 submitRunnableAndHandleExceptions( 109 "Failed to reconcile application update.", 110 () -> TopicsWorker.getInstance().reconcileApplicationUpdate(this)); 111 } 112 113 ListenableFuture<Boolean> fledgeMaintenanceTasksFuture; 114 if (flags.getFledgeSelectAdsKillSwitch()) { 115 LogUtil.d("Ad Selection API is disabled, skipping Ad Selection Maintenance Job"); 116 fledgeMaintenanceTasksFuture = Futures.immediateFuture(true); 117 } else { 118 fledgeMaintenanceTasksFuture = 119 submitRunnableAndHandleExceptions( 120 "Failed to complete Ad Selection data maintenance tasks.", 121 this::doAdSelectionDataMaintenanceTasks); 122 } 123 124 ListenableFuture<Boolean> protectedSignalsMaintenanceTasksFuture; 125 if (!flags.getProtectedSignalsCleanupEnabled()) { 126 LogUtil.d("Signals cleanup is disabled, skipping maintenance job"); 127 protectedSignalsMaintenanceTasksFuture = Futures.immediateFuture(true); 128 } else { 129 protectedSignalsMaintenanceTasksFuture = 130 submitRunnableAndHandleExceptions( 131 "Failed to complete Protected Signals data maintenance tasks.", 132 this::doProtectedSignalsDataMaintenanceTasks); 133 } 134 135 ListenableFuture<List<Boolean>> futuresList = 136 Futures.allAsList( 137 fledgeMaintenanceTasksFuture, 138 protectedSignalsMaintenanceTasksFuture, 139 appReconciliationFuture); 140 141 Futures.addCallback( 142 futuresList, 143 new FutureCallback<>() { 144 @Override 145 public void onSuccess(List<Boolean> result) { 146 boolean shouldRetry = false; 147 boolean isSuccessful = result.stream().allMatch(b -> b); 148 149 if (isSuccessful) { 150 LogUtil.d("PP API jobs are done!"); 151 } else { 152 LogUtil.e( 153 "Failed to handle MaintenanceJobService: " + params.getJobId()); 154 } 155 156 AdServicesJobServiceLogger.getInstance() 157 .recordJobFinished(MAINTENANCE_JOB_ID, isSuccessful, shouldRetry); 158 159 jobFinished(params, shouldRetry); 160 } 161 162 @Override 163 public void onFailure(Throwable t) { 164 boolean shouldRetry = false; 165 AdServicesJobServiceLogger.getInstance() 166 .recordJobFinished( 167 MAINTENANCE_JOB_ID, /* isSuccessful= */ false, shouldRetry); 168 169 LogUtil.e( 170 t, "Failed to handle MaintenanceJobService: " + params.getJobId()); 171 jobFinished(params, shouldRetry); 172 } 173 }, 174 directExecutor()); 175 return true; 176 } 177 178 @Override onStopJob(JobParameters params)179 public boolean onStopJob(JobParameters params) { 180 LogUtil.d("MaintenanceJobService.onStopJob"); 181 182 // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the 183 // execution is completed or not to avoid executing the task twice. 184 boolean shouldRetry = false; 185 186 AdServicesJobServiceLogger.getInstance() 187 .recordOnStopJob(params, MAINTENANCE_JOB_ID, shouldRetry); 188 return shouldRetry; 189 } 190 submitRunnableAndHandleExceptions( String message, Runnable runnable)191 private ListenableFuture<Boolean> submitRunnableAndHandleExceptions( 192 String message, Runnable runnable) { 193 return Futures.submit( 194 () -> { 195 try { 196 runnable.run(); 197 return true; 198 } catch (Exception e) { 199 LogUtil.e(e, message); 200 return false; 201 } 202 }, 203 AdServicesExecutors.getBackgroundExecutor()); 204 } 205 206 @VisibleForTesting 207 // TODO(b/311183933): Remove passed in Context from static method. 208 @SuppressWarnings("AvoidStaticContext") 209 static void schedule( 210 Context context, 211 @NonNull JobScheduler jobScheduler, 212 long maintenanceJobPeriodMs, 213 long maintenanceJobFlexMs) { 214 final JobInfo job = 215 new JobInfo.Builder( 216 MAINTENANCE_JOB_ID, 217 new ComponentName(context, MaintenanceJobService.class)) 218 .setRequiresCharging(true) 219 .setPersisted(true) 220 .setPeriodic(maintenanceJobPeriodMs, maintenanceJobFlexMs) 221 .build(); 222 223 jobScheduler.schedule(job); 224 LogUtil.d("Scheduling maintenance job ..."); 225 } 226 227 /** 228 * Schedule Maintenance Job Service if needed: there is no scheduled job with same job 229 * parameters. 230 * 231 * @param context the context 232 * @param forceSchedule a flag to indicate whether to force rescheduling the job. 233 * @return a {@code boolean} to indicate if the service job is actually scheduled. 234 */ 235 // TODO(b/311183933): Remove passed in Context from static method. 236 @SuppressWarnings("AvoidStaticContext") 237 public static boolean scheduleIfNeeded(Context context, boolean forceSchedule) { 238 Flags flags = FlagsFactory.getFlags(); 239 240 if (flags.getTopicsKillSwitch() && flags.getFledgeSelectAdsKillSwitch()) { 241 LogUtil.e( 242 "Both Topics and Select Ads are disabled, skipping scheduling" 243 + " MaintenanceJobService"); 244 return false; 245 } 246 247 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 248 if (jobScheduler == null) { 249 LogUtil.e("Cannot fetch Job Scheduler!"); 250 return false; 251 } 252 253 long flagsMaintenanceJobPeriodMs = flags.getMaintenanceJobPeriodMs(); 254 long flagsMaintenanceJobFlexMs = flags.getMaintenanceJobFlexMs(); 255 256 JobInfo job = jobScheduler.getPendingJob(MAINTENANCE_JOB_ID); 257 // Skip to reschedule the job if there is same scheduled job with same parameters. 258 if (job != null && !forceSchedule) { 259 long maintenanceJobPeriodMs = job.getIntervalMillis(); 260 long maintenanceJobFlexMs = job.getFlexMillis(); 261 262 if (flagsMaintenanceJobPeriodMs == maintenanceJobPeriodMs 263 && flagsMaintenanceJobFlexMs == maintenanceJobFlexMs) { 264 LogUtil.d( 265 "Maintenance Job Service has been scheduled with same parameters, skip" 266 + " rescheduling!"); 267 return false; 268 } 269 } 270 271 schedule(context, jobScheduler, flagsMaintenanceJobPeriodMs, flagsMaintenanceJobFlexMs); 272 return true; 273 } 274 275 private boolean skipAndCancelBackgroundJob( 276 final JobParameters params, int skipReason, boolean doRecord) { 277 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 278 if (jobScheduler != null) { 279 jobScheduler.cancel(MAINTENANCE_JOB_ID); 280 } 281 282 if (doRecord) { 283 AdServicesJobServiceLogger.getInstance() 284 .recordJobSkipped(MAINTENANCE_JOB_ID, skipReason); 285 } 286 287 // Tell the JobScheduler that the job has completed and does not need to be 288 // rescheduled. 289 jobFinished(params, false); 290 291 // Returning false means that this job has completed its work. 292 return false; 293 } 294 295 private FledgeMaintenanceTasksWorker getFledgeMaintenanceTasksWorker() { 296 if (!Objects.isNull(mFledgeMaintenanceTasksWorker)) { 297 return mFledgeMaintenanceTasksWorker; 298 } 299 mFledgeMaintenanceTasksWorker = FledgeMaintenanceTasksWorker.create(this); 300 return mFledgeMaintenanceTasksWorker; 301 } 302 303 private SignalsMaintenanceTasksWorker getSignalsMaintenanceTasksWorker() { 304 if (!Objects.isNull(mSignalsMaintenanceTasksWorker)) { 305 return mSignalsMaintenanceTasksWorker; 306 } 307 mSignalsMaintenanceTasksWorker = SignalsMaintenanceTasksWorker.create(this); 308 return mSignalsMaintenanceTasksWorker; 309 } 310 311 private void doAdSelectionDataMaintenanceTasks() { 312 LogUtil.v("Performing Ad Selection maintenance tasks"); 313 getFledgeMaintenanceTasksWorker().clearExpiredAdSelectionData(); 314 getFledgeMaintenanceTasksWorker() 315 .clearInvalidFrequencyCapHistogramData(this.getPackageManager()); 316 getFledgeMaintenanceTasksWorker().clearExpiredKAnonMessageEntities(); 317 } 318 319 private void doProtectedSignalsDataMaintenanceTasks() { 320 LogUtil.v("Performing protected signals maintenance tasks"); 321 getSignalsMaintenanceTasksWorker().clearInvalidProtectedSignalsData(); 322 } 323 } 324