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