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.topics; 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_ERROR_REPORTED__ERROR_CODE__TOPICS_API_DISABLED; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_FETCH_JOB_SCHEDULER_FAILURE; 22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_HANDLE_JOB_SERVICE_FAILURE; 23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS; 24 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED; 25 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED; 26 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL; 27 import static com.android.adservices.spe.AdServicesJobInfo.TOPICS_EPOCH_JOB; 28 29 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 30 31 import android.annotation.NonNull; 32 import android.app.job.JobInfo; 33 import android.app.job.JobParameters; 34 import android.app.job.JobScheduler; 35 import android.app.job.JobService; 36 import android.content.ComponentName; 37 import android.content.Context; 38 import android.os.Build; 39 40 import androidx.annotation.RequiresApi; 41 42 import com.android.adservices.LogUtil; 43 import com.android.adservices.LoggerFactory; 44 import com.android.adservices.concurrency.AdServicesExecutors; 45 import com.android.adservices.errorlogging.ErrorLogUtil; 46 import com.android.adservices.service.FlagsFactory; 47 import com.android.adservices.service.common.compat.ServiceCompatUtils; 48 import com.android.adservices.shared.common.ApplicationContextSingleton; 49 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode; 50 import com.android.adservices.spe.AdServicesJobServiceLogger; 51 import com.android.internal.annotations.VisibleForTesting; 52 53 import com.google.common.util.concurrent.FutureCallback; 54 import com.google.common.util.concurrent.Futures; 55 import com.google.common.util.concurrent.ListenableFuture; 56 57 /** Epoch computation job. This will be run approximately once per epoch to compute Topics. */ 58 @RequiresApi(Build.VERSION_CODES.S) 59 public final class EpochJobService extends JobService { 60 private static final int TOPICS_EPOCH_JOB_ID = TOPICS_EPOCH_JOB.getJobId(); 61 62 @Override onStartJob(JobParameters params)63 public boolean onStartJob(JobParameters params) { 64 // Always ensure that the first thing this job does is check if it should be running, and 65 // cancel itself if it's not supposed to be. 66 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 67 LogUtil.d("Disabling EpochJobService job because it's running in ExtServices on T+"); 68 return skipAndCancelBackgroundJob(params, /* skipReason= */ 0, /* doRecord= */ false); 69 } 70 71 LoggerFactory.getTopicsLogger().d("EpochJobService.onStartJob"); 72 73 AdServicesJobServiceLogger.getInstance().recordOnStartJob(TOPICS_EPOCH_JOB_ID); 74 75 if (FlagsFactory.getFlags().getTopicsKillSwitch()) { 76 ErrorLogUtil.e( 77 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_API_DISABLED, 78 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 79 LoggerFactory.getTopicsLogger() 80 .e("Topics API is disabled, skipping and cancelling EpochJobService"); 81 return skipAndCancelBackgroundJob( 82 params, 83 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 84 /* doRecord= */ true); 85 } 86 87 // This service executes each incoming job on a Handler running on the application's 88 // main thread. This means that we must offload the execution logic to background executor. 89 // TODO(b/225382268): Handle cancellation. 90 ListenableFuture<Void> epochComputationFuture = 91 Futures.submit( 92 () -> TopicsWorker.getInstance().computeEpoch(), 93 AdServicesExecutors.getBackgroundExecutor()); 94 95 Futures.addCallback( 96 epochComputationFuture, 97 new FutureCallback<>() { 98 @Override 99 public void onSuccess(Void result) { 100 LoggerFactory.getTopicsLogger().d("Epoch Computation succeeded!"); 101 102 boolean shouldRetry = false; 103 AdServicesJobServiceLogger.getInstance() 104 .recordJobFinished( 105 TOPICS_EPOCH_JOB_ID, /* isSuccessful= */ true, shouldRetry); 106 107 // Tell the JobScheduler that the job has completed and does not need to be 108 // rescheduled. 109 jobFinished(params, shouldRetry); 110 } 111 112 @Override 113 public void onFailure(Throwable t) { 114 ErrorLogUtil.e( 115 t, 116 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_HANDLE_JOB_SERVICE_FAILURE, 117 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 118 LoggerFactory.getTopicsLogger() 119 .e(t, "Failed to handle JobService: " + params.getJobId()); 120 121 boolean shouldRetry = false; 122 AdServicesJobServiceLogger.getInstance() 123 .recordJobFinished( 124 TOPICS_EPOCH_JOB_ID, 125 /* isSuccessful= */ false, 126 shouldRetry); 127 128 // When failure, also tell the JobScheduler that the job has completed and 129 // does not need to be rescheduled. 130 // TODO(b/225909845): Revisit this. We need a retry policy. 131 jobFinished(params, shouldRetry); 132 } 133 }, 134 directExecutor()); 135 136 // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this EpochJobService 137 // will be cancelled for the same job ID. 138 // 139 // Also for a job with Flex Period, it will NOT execute immediately after rescheduling it. 140 // Reschedule it here to let the execution complete and the next cycle will execute with 141 // the EpochJob.schedule(). 142 if (FlagsFactory.getFlags().getSpeOnEpochJobEnabled()) { 143 LoggerFactory.getTopicsLogger() 144 .d("SPE is enabled. Reschedule EpochJob with SPE framework."); 145 EpochJob.schedule(); 146 } 147 148 return true; 149 } 150 151 @Override onStopJob(JobParameters params)152 public boolean onStopJob(JobParameters params) { 153 LoggerFactory.getTopicsLogger().d("EpochJobService.onStopJob"); 154 155 // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the 156 // execution is completed or not to avoid executing the task twice. 157 boolean shouldRetry = false; 158 159 AdServicesJobServiceLogger.getInstance() 160 .recordOnStopJob(params, TOPICS_EPOCH_JOB_ID, shouldRetry); 161 return shouldRetry; 162 } 163 164 @VisibleForTesting schedule( Context context, @NonNull JobScheduler jobScheduler, long epochJobPeriodMs, long epochJobFlexMs)165 static void schedule( 166 Context context, 167 @NonNull JobScheduler jobScheduler, 168 long epochJobPeriodMs, 169 long epochJobFlexMs) { 170 final JobInfo job = 171 new JobInfo.Builder( 172 TOPICS_EPOCH_JOB_ID, 173 new ComponentName(context, EpochJobService.class)) 174 .setRequiresCharging(true) 175 .setPersisted(true) 176 .setPeriodic(epochJobPeriodMs, epochJobFlexMs) 177 .build(); 178 179 jobScheduler.schedule(job); 180 LoggerFactory.getTopicsLogger().d("Scheduling Epoch job ..."); 181 } 182 183 /** 184 * Schedule Epoch Job Service if needed: there is no scheduled job with same job parameters. 185 * 186 * @param forceSchedule a flag to indicate whether to force rescheduling the job. 187 * @return a {@code boolean} to indicate if the service job is actually scheduled. 188 */ 189 @JobSchedulingResultCode scheduleIfNeeded(boolean forceSchedule)190 public static int scheduleIfNeeded(boolean forceSchedule) { 191 Context context = ApplicationContextSingleton.get(); 192 193 if (FlagsFactory.getFlags().getTopicsKillSwitch()) { 194 ErrorLogUtil.e( 195 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_API_DISABLED, 196 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 197 LoggerFactory.getTopicsLogger() 198 .e("Topics API is disabled, skip scheduling the EpochJobService"); 199 return SCHEDULING_RESULT_CODE_SKIPPED; 200 } 201 202 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 203 if (jobScheduler == null) { 204 ErrorLogUtil.e( 205 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_FETCH_JOB_SCHEDULER_FAILURE, 206 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 207 LoggerFactory.getTopicsLogger().e("Cannot fetch Job Scheduler!"); 208 return SCHEDULING_RESULT_CODE_FAILED; 209 } 210 211 long flagsEpochJobPeriodMs = FlagsFactory.getFlags().getTopicsEpochJobPeriodMs(); 212 long flagsEpochJobFlexMs = FlagsFactory.getFlags().getTopicsEpochJobFlexMs(); 213 214 JobInfo job = jobScheduler.getPendingJob(TOPICS_EPOCH_JOB_ID); 215 // Skip to reschedule the job if there is same scheduled job with same parameters. 216 if (job != null && !forceSchedule) { 217 long epochJobPeriodMs = job.getIntervalMillis(); 218 long epochJobFlexMs = job.getFlexMillis(); 219 220 if (flagsEpochJobPeriodMs == epochJobPeriodMs 221 && flagsEpochJobFlexMs == epochJobFlexMs) { 222 LoggerFactory.getTopicsLogger() 223 .i( 224 "Epoch Job Service has been scheduled with same parameters, skip" 225 + " rescheduling!"); 226 return SCHEDULING_RESULT_CODE_SKIPPED; 227 } 228 } 229 230 schedule(context, jobScheduler, flagsEpochJobPeriodMs, flagsEpochJobFlexMs); 231 return SCHEDULING_RESULT_CODE_SUCCESSFUL; 232 } 233 skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)234 private boolean skipAndCancelBackgroundJob( 235 final JobParameters params, int skipReason, boolean doRecord) { 236 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 237 if (jobScheduler != null) { 238 jobScheduler.cancel(TOPICS_EPOCH_JOB_ID); 239 } 240 241 if (doRecord) { 242 AdServicesJobServiceLogger.getInstance() 243 .recordJobSkipped(TOPICS_EPOCH_JOB_ID, skipReason); 244 } 245 246 // Tell the JobScheduler that the job has completed and does not need to be 247 // rescheduled. 248 jobFinished(params, false); 249 250 // Returning false means that this job has completed its work. 251 return false; 252 } 253 } 254