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_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.spe.AdservicesJobInfo.MAINTENANCE_JOB; 22 23 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 24 25 import android.annotation.NonNull; 26 import android.app.job.JobInfo; 27 import android.app.job.JobParameters; 28 import android.app.job.JobScheduler; 29 import android.app.job.JobService; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.os.Build; 33 34 import androidx.annotation.RequiresApi; 35 36 import com.android.adservices.LogUtil; 37 import com.android.adservices.concurrency.AdServicesExecutors; 38 import com.android.adservices.service.common.FledgeMaintenanceTasksWorker; 39 import com.android.adservices.service.common.compat.ServiceCompatUtils; 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 // TODO(b/269798827): Enable for R. 53 @RequiresApi(Build.VERSION_CODES.S) 54 public final class MaintenanceJobService extends JobService { 55 private static final int MAINTENANCE_JOB_ID = MAINTENANCE_JOB.getJobId(); 56 57 private FledgeMaintenanceTasksWorker mFledgeMaintenanceTasksWorker; 58 59 /** Injects a {@link FledgeMaintenanceTasksWorker to be used during testing} */ 60 @VisibleForTesting injectFledgeMaintenanceTasksWorker( @onNull FledgeMaintenanceTasksWorker fledgeMaintenanceTasksWorker)61 public void injectFledgeMaintenanceTasksWorker( 62 @NonNull FledgeMaintenanceTasksWorker fledgeMaintenanceTasksWorker) { 63 mFledgeMaintenanceTasksWorker = fledgeMaintenanceTasksWorker; 64 } 65 66 @Override onStartJob(JobParameters params)67 public boolean onStartJob(JobParameters params) { 68 // Always ensure that the first thing this job does is check if it should be running, and 69 // cancel itself if it's not supposed to be. 70 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 71 LogUtil.d( 72 "Disabling MaintenanceJobService job because it's running in ExtServices on" 73 + " T+"); 74 return skipAndCancelBackgroundJob( 75 params, 76 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS); 77 } 78 79 LogUtil.d("MaintenanceJobService.onStartJob"); 80 AdservicesJobServiceLogger.getInstance(this).recordOnStartJob(MAINTENANCE_JOB_ID); 81 82 if (FlagsFactory.getFlags().getTopicsKillSwitch() 83 && FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) { 84 LogUtil.e( 85 "Both Topics and Select Ads are disabled, skipping and cancelling" 86 + " MaintenanceJobService"); 87 return skipAndCancelBackgroundJob( 88 params, 89 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON); 90 } 91 92 ListenableFuture<Void> appReconciliationFuture; 93 if (FlagsFactory.getFlags().getTopicsKillSwitch()) { 94 LogUtil.d("Topics API is disabled, skipping Topics Job"); 95 appReconciliationFuture = Futures.immediateFuture(null); 96 } else { 97 appReconciliationFuture = 98 Futures.submit( 99 () -> TopicsWorker.getInstance(this).reconcileApplicationUpdate(this), 100 AdServicesExecutors.getBackgroundExecutor()); 101 } 102 103 ListenableFuture<Void> fledgeMaintenanceTasksFuture; 104 if (FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) { 105 LogUtil.d("SelectAds API is disabled, skipping SelectAds Job"); 106 fledgeMaintenanceTasksFuture = Futures.immediateFuture(null); 107 } else { 108 fledgeMaintenanceTasksFuture = 109 Futures.submit( 110 this::doAdSelectionDataMaintenanceTasks, 111 AdServicesExecutors.getBackgroundExecutor()); 112 } 113 114 ListenableFuture<List<Void>> futuresList = 115 Futures.allAsList(fledgeMaintenanceTasksFuture, appReconciliationFuture); 116 117 Futures.addCallback( 118 futuresList, 119 new FutureCallback<List<Void>>() { 120 @Override 121 public void onSuccess(List<Void> result) { 122 boolean shouldRetry = false; 123 AdservicesJobServiceLogger.getInstance(MaintenanceJobService.this) 124 .recordJobFinished( 125 MAINTENANCE_JOB_ID, /* isSuccessful= */ true, shouldRetry); 126 127 LogUtil.d("PP API jobs are done!"); 128 jobFinished(params, shouldRetry); 129 } 130 131 @Override 132 public void onFailure(Throwable t) { 133 boolean shouldRetry = false; 134 AdservicesJobServiceLogger.getInstance(MaintenanceJobService.this) 135 .recordJobFinished( 136 MAINTENANCE_JOB_ID, /* isSuccessful= */ false, shouldRetry); 137 138 LogUtil.e( 139 t, "Failed to handle MaintenanceJobService: " + params.getJobId()); 140 jobFinished(params, shouldRetry); 141 } 142 }, 143 directExecutor()); 144 return true; 145 } 146 147 @Override onStopJob(JobParameters params)148 public boolean onStopJob(JobParameters params) { 149 LogUtil.d("MaintenanceJobService.onStopJob"); 150 151 // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the 152 // execution is completed or not to avoid executing the task twice. 153 boolean shouldRetry = false; 154 155 AdservicesJobServiceLogger.getInstance(this) 156 .recordOnStopJob(params, MAINTENANCE_JOB_ID, shouldRetry); 157 return shouldRetry; 158 } 159 160 @VisibleForTesting schedule( Context context, @NonNull JobScheduler jobScheduler, long maintenanceJobPeriodMs, long maintenanceJobFlexMs)161 static void schedule( 162 Context context, 163 @NonNull JobScheduler jobScheduler, 164 long maintenanceJobPeriodMs, 165 long maintenanceJobFlexMs) { 166 final JobInfo job = 167 new JobInfo.Builder( 168 MAINTENANCE_JOB_ID, 169 new ComponentName(context, MaintenanceJobService.class)) 170 .setRequiresCharging(true) 171 .setPersisted(true) 172 .setPeriodic(maintenanceJobPeriodMs, maintenanceJobFlexMs) 173 .build(); 174 175 jobScheduler.schedule(job); 176 LogUtil.d("Scheduling maintenance job ..."); 177 } 178 179 /** 180 * Schedule Maintenance Job Service if needed: there is no scheduled job with same job 181 * parameters. 182 * 183 * @param context the context 184 * @param forceSchedule a flag to indicate whether to force rescheduling the job. 185 * @return a {@code boolean} to indicate if the service job is actually scheduled. 186 */ scheduleIfNeeded(Context context, boolean forceSchedule)187 public static boolean scheduleIfNeeded(Context context, boolean forceSchedule) { 188 if (FlagsFactory.getFlags().getTopicsKillSwitch() 189 && FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) { 190 LogUtil.e( 191 "Both Topics and Select Ads are disabled, skipping scheduling" 192 + " MaintenanceJobService"); 193 return false; 194 } 195 196 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 197 if (jobScheduler == null) { 198 LogUtil.e("Cannot fetch Job Scheduler!"); 199 return false; 200 } 201 202 long flagsMaintenanceJobPeriodMs = FlagsFactory.getFlags().getMaintenanceJobPeriodMs(); 203 long flagsMaintenanceJobFlexMs = FlagsFactory.getFlags().getMaintenanceJobFlexMs(); 204 205 JobInfo job = jobScheduler.getPendingJob(MAINTENANCE_JOB_ID); 206 // Skip to reschedule the job if there is same scheduled job with same parameters. 207 if (job != null && !forceSchedule) { 208 long maintenanceJobPeriodMs = job.getIntervalMillis(); 209 long maintenanceJobFlexMs = job.getFlexMillis(); 210 211 if (flagsMaintenanceJobPeriodMs == maintenanceJobPeriodMs 212 && flagsMaintenanceJobFlexMs == maintenanceJobFlexMs) { 213 LogUtil.i( 214 "Maintenance Job Service has been scheduled with same parameters, skip" 215 + " rescheduling!"); 216 return false; 217 } 218 } 219 220 schedule(context, jobScheduler, flagsMaintenanceJobPeriodMs, flagsMaintenanceJobFlexMs); 221 return true; 222 } 223 skipAndCancelBackgroundJob(final JobParameters params, int skipReason)224 private boolean skipAndCancelBackgroundJob(final JobParameters params, int skipReason) { 225 this.getSystemService(JobScheduler.class).cancel(MAINTENANCE_JOB_ID); 226 227 AdservicesJobServiceLogger.getInstance(this) 228 .recordJobSkipped(MAINTENANCE_JOB_ID, skipReason); 229 230 // Tell the JobScheduler that the job has completed and does not need to be 231 // rescheduled. 232 jobFinished(params, false); 233 234 // Returning false means that this job has completed its work. 235 return false; 236 } 237 getFledgeMaintenanceTasksWorker()238 private FledgeMaintenanceTasksWorker getFledgeMaintenanceTasksWorker() { 239 if (!Objects.isNull(mFledgeMaintenanceTasksWorker)) { 240 return mFledgeMaintenanceTasksWorker; 241 } 242 mFledgeMaintenanceTasksWorker = FledgeMaintenanceTasksWorker.create(this); 243 return mFledgeMaintenanceTasksWorker; 244 } 245 doAdSelectionDataMaintenanceTasks()246 private void doAdSelectionDataMaintenanceTasks() { 247 LogUtil.v("Performing Ad Selection maintenance tasks"); 248 getFledgeMaintenanceTasksWorker().clearExpiredAdSelectionData(); 249 } 250 } 251