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.download; 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.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED; 21 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED; 22 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL; 23 import static com.android.adservices.spe.AdServicesJobInfo.MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB; 24 import static com.android.adservices.spe.AdServicesJobInfo.MDD_CHARGING_PERIODIC_TASK_JOB; 25 import static com.android.adservices.spe.AdServicesJobInfo.MDD_MAINTENANCE_PERIODIC_TASK_JOB; 26 import static com.android.adservices.spe.AdServicesJobInfo.MDD_WIFI_CHARGING_PERIODIC_TASK_JOB; 27 28 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 29 30 import android.annotation.NonNull; 31 import android.app.job.JobInfo; 32 import android.app.job.JobParameters; 33 import android.app.job.JobScheduler; 34 import android.app.job.JobService; 35 import android.content.ComponentName; 36 import android.content.Context; 37 import android.os.Build; 38 import android.os.PersistableBundle; 39 40 import androidx.annotation.RequiresApi; 41 42 import com.android.adservices.LogUtil; 43 import com.android.adservices.concurrency.AdServicesExecutors; 44 import com.android.adservices.service.FlagsFactory; 45 import com.android.adservices.service.common.compat.ServiceCompatUtils; 46 import com.android.adservices.shared.common.ApplicationContextSingleton; 47 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode; 48 import com.android.adservices.spe.AdServicesJobServiceLogger; 49 import com.android.internal.annotations.VisibleForTesting; 50 51 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 52 import com.google.common.util.concurrent.FutureCallback; 53 import com.google.common.util.concurrent.Futures; 54 import com.google.common.util.concurrent.ListenableFuture; 55 56 import java.util.concurrent.Executor; 57 58 /** MDD JobService. This will download MDD files in background tasks. */ 59 // TODO(b/269798827): Enable for R. 60 @RequiresApi(Build.VERSION_CODES.S) 61 public class MddJobService extends JobService { 62 static final String KEY_MDD_TASK_TAG = "mdd_task_tag"; 63 64 /** 65 * Tag for daily mdd maintenance task, that *should* be run once and only once every 24 hours. 66 * 67 * <p>By default, this task runs on charging. 68 */ 69 static final String MAINTENANCE_PERIODIC_TASK = "MDD.MAINTENANCE.PERIODIC.GCM.TASK"; 70 71 /** 72 * Tag for mdd task that doesn't require any network. This is used to perform some routine 73 * operation that do not require network, in case a device doesn't connect to any network for a 74 * long time. 75 * 76 * <p>By default, this task runs on charging once every 6 hours. 77 */ 78 static final String CHARGING_PERIODIC_TASK = "MDD.CHARGING.PERIODIC.TASK"; 79 80 /** 81 * Tag for mdd task that runs on cellular network. This is used to primarily download file 82 * groups that can be downloaded on cellular network. 83 * 84 * <p>By default, this task runs on charging once every 6 hours. This task can be skipped if 85 * nothing is downloaded on cellular. 86 */ 87 static final String CELLULAR_CHARGING_PERIODIC_TASK = "MDD.CELLULAR.CHARGING.PERIODIC.TASK"; 88 89 /** 90 * Tag for mdd task that runs on Wi-Fi network. This is used to primarily download file groups 91 * that can be downloaded only on Wi-Fi network. 92 * 93 * <p>By default, this task runs on charging once every 6 hours. This task can be skipped if 94 * nothing is restricted to Wi-Fi. 95 */ 96 static final String WIFI_CHARGING_PERIODIC_TASK = "MDD.WIFI.CHARGING.PERIODIC.TASK"; 97 98 /** Required network state of the device when to run the task. */ 99 enum NetworkState { 100 // Metered or unmetered network available. 101 NETWORK_STATE_CONNECTED, 102 103 // Unmetered network available. 104 NETWORK_STATE_UNMETERED, 105 106 // Network not required. 107 NETWORK_STATE_ANY, 108 } 109 110 private static final long MILLISECONDS_IN_SECOND = 1000L; 111 112 private static final Executor sBlockingExecutor = AdServicesExecutors.getBlockingExecutor(); 113 114 @Override onStartJob(@onNull JobParameters params)115 public boolean onStartJob(@NonNull JobParameters params) { 116 // Always ensure that the first thing this job does is check if it should be running, and 117 // cancel itself if it's not supposed to be. 118 int jobId = getMddTaskJobId(getMddTag(params)); 119 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 120 LogUtil.d("Disabling MddJobService job because it's running in ExtServices on T+"); 121 return skipAndCancelBackgroundJob( 122 params, jobId, /* skipReason= */ 0, /* doRecord= */ false); 123 } 124 125 // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this MddJobService will 126 // be cancelled for the same job ID. 127 if (FlagsFactory.getFlags().getSpeOnPilotJobsEnabled()) { 128 MddJob.scheduleAllMddJobs(); 129 return false; 130 } 131 132 // Record the invocation of onStartJob() for logging purpose. 133 LogUtil.d("MddJobService.onStartJob"); 134 AdServicesJobServiceLogger.getInstance().recordOnStartJob(jobId); 135 136 if (FlagsFactory.getFlags().getMddBackgroundTaskKillSwitch()) { 137 LogUtil.e("MDD background task is disabled, skipping and cancelling MddJobService"); 138 return skipAndCancelBackgroundJob( 139 params, 140 jobId, 141 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 142 /* doRecord= */ true); 143 } 144 145 // This service executes each incoming job on a Handler running on the application's 146 // main thread. This means that we must offload the execution logic to background executor. 147 ListenableFuture<Void> handleTaskFuture = 148 PropagatedFutures.submitAsync( 149 () -> { 150 String mddTag = getMddTag(params); 151 LogUtil.d("MddJobService.onStartJob for " + mddTag); 152 153 return MobileDataDownloadFactory.getMdd(FlagsFactory.getFlags()) 154 .handleTask(mddTag); 155 }, 156 AdServicesExecutors.getBackgroundExecutor()); 157 158 Futures.addCallback( 159 handleTaskFuture, 160 new FutureCallback<Void>() { 161 @Override 162 public void onSuccess(Void result) { 163 LogUtil.v("MddJobService.MddHandleTask succeeded!"); 164 165 sBlockingExecutor.execute( 166 () -> { 167 EnrollmentDataDownloadManager.getInstance() 168 .readAndInsertEnrollmentDataFromMdd(); 169 ListenableFuture<EncryptionDataDownloadManager.DownloadStatus> 170 unused = 171 EncryptionDataDownloadManager.getInstance() 172 .readAndInsertEncryptionDataFromMdd(); 173 174 // Logging has to happen before jobFinished() is called. Due to 175 // JobScheduler infra, the JobService instance will end its 176 // lifecycle (call onDestroy()) once jobFinished() is invoked. 177 // 178 // Tell the JobScheduler that the job has completed and does not 179 // need to be rescheduled. 180 boolean shouldRetry = false; 181 AdServicesJobServiceLogger.getInstance() 182 .recordJobFinished( 183 jobId, /* isSuccessful= */ true, shouldRetry); 184 185 jobFinished(params, shouldRetry); 186 }); 187 } 188 189 @Override 190 public void onFailure(Throwable t) { 191 LogUtil.e(t, "Failed to handle JobService: " + params.getJobId()); 192 193 // Logging has to happen before jobFinished() is called. Due to 194 // JobScheduler infra, the JobService instance will end its 195 // lifecycle (call onDestroy()) once jobFinished() is invoked. 196 // 197 // When failure, also tell the JobScheduler that the job has completed and 198 // does not need to be rescheduled. 199 boolean shouldRetry = false; 200 AdServicesJobServiceLogger.getInstance() 201 .recordJobFinished(jobId, /* isSuccessful= */ false, shouldRetry); 202 203 jobFinished(params, shouldRetry); 204 } 205 }, 206 directExecutor()); 207 208 return true; 209 } 210 211 @Override onStopJob(@onNull JobParameters params)212 public boolean onStopJob(@NonNull JobParameters params) { 213 LogUtil.d("MddJobService.onStopJob"); 214 215 // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the 216 // execution is completed or not to avoid executing the task twice. 217 boolean shouldRetry = false; 218 219 AdServicesJobServiceLogger.getInstance() 220 .recordOnStopJob(params, getMddTaskJobId(getMddTag(params)), shouldRetry); 221 return shouldRetry; 222 } 223 224 /** 225 * Schedule MddJobService if needed: there is no scheduled job with same job parameters. 226 * 227 * @param forceSchedule a flag to indicate whether to force rescheduling the job. 228 */ 229 @JobSchedulingResultCode scheduleIfNeeded(boolean forceSchedule)230 public static int scheduleIfNeeded(boolean forceSchedule) { 231 if (FlagsFactory.getFlags().getMddBackgroundTaskKillSwitch()) { 232 LogUtil.e("Mdd background task is disabled, skip scheduling."); 233 return SCHEDULING_RESULT_CODE_SKIPPED; 234 } 235 236 Context context = ApplicationContextSingleton.get(); 237 238 final JobScheduler jobscheduler = context.getSystemService(JobScheduler.class); 239 if (jobscheduler == null) { 240 LogUtil.e("Cannot fetch Job Scheduler!"); 241 return SCHEDULING_RESULT_CODE_FAILED; 242 } 243 244 // Assign boolean local variable to each task to prevent short-circuit following tasks. 245 boolean isMaintenancePeriodicTaskScheduled = 246 scheduleIfNeededMddSingleTask( 247 context, 248 forceSchedule, 249 MAINTENANCE_PERIODIC_TASK, 250 NetworkState.NETWORK_STATE_ANY, 251 jobscheduler); 252 boolean isChargingPeriodicTaskScheduled = 253 scheduleIfNeededMddSingleTask( 254 context, 255 forceSchedule, 256 CHARGING_PERIODIC_TASK, 257 NetworkState.NETWORK_STATE_ANY, 258 jobscheduler); 259 boolean isCellularChargingPeriodicTaskScheduled = 260 scheduleIfNeededMddSingleTask( 261 context, 262 forceSchedule, 263 CELLULAR_CHARGING_PERIODIC_TASK, 264 NetworkState.NETWORK_STATE_CONNECTED, 265 jobscheduler); 266 boolean isWifiChargingPeriodicTaskScheduled = 267 scheduleIfNeededMddSingleTask( 268 context, 269 forceSchedule, 270 WIFI_CHARGING_PERIODIC_TASK, 271 NetworkState.NETWORK_STATE_UNMETERED, 272 jobscheduler); 273 274 return isMaintenancePeriodicTaskScheduled 275 && isChargingPeriodicTaskScheduled 276 && isCellularChargingPeriodicTaskScheduled 277 && isWifiChargingPeriodicTaskScheduled 278 ? SCHEDULING_RESULT_CODE_SUCCESSFUL 279 : SCHEDULING_RESULT_CODE_SKIPPED; 280 } 281 282 /** 283 * Unscheduled all jobs in MddJobService. 284 * 285 * @param jobScheduler Job scheduler to cancel the jobs 286 */ unscheduleAllJobs(@onNull JobScheduler jobScheduler)287 public static void unscheduleAllJobs(@NonNull JobScheduler jobScheduler) { 288 jobScheduler.cancel(MDD_MAINTENANCE_PERIODIC_TASK_JOB.getJobId()); 289 jobScheduler.cancel(MDD_CHARGING_PERIODIC_TASK_JOB.getJobId()); 290 jobScheduler.cancel(MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB.getJobId()); 291 jobScheduler.cancel(MDD_WIFI_CHARGING_PERIODIC_TASK_JOB.getJobId()); 292 } 293 294 /** 295 * Schedule Mdd task using jobScheduler. 296 * 297 * @param context the context. 298 * @param forceSchedule a flag to indicate whether to force rescheduling the job. 299 * @param mddTag a String to indicate Mdd job. 300 * @param networkState the NetworkState for setting RequiredNetworkType. 301 * @param jobScheduler a job scheduler to schedule the job. 302 * @return true if task scheduled successfully, otherwise, return false. 303 */ 304 @VisibleForTesting scheduleIfNeededMddSingleTask( Context context, boolean forceSchedule, String mddTag, NetworkState networkState, JobScheduler jobScheduler)305 static boolean scheduleIfNeededMddSingleTask( 306 Context context, 307 boolean forceSchedule, 308 String mddTag, 309 NetworkState networkState, 310 JobScheduler jobScheduler) { 311 312 long taskPeriodMs = 0; 313 MddFlags mddFlags = MddFlags.getInstance(); 314 switch (mddTag) { 315 case MAINTENANCE_PERIODIC_TASK: 316 taskPeriodMs = /* This flag is in second. */ 317 mddFlags.maintenanceGcmTaskPeriod() * MILLISECONDS_IN_SECOND; 318 break; 319 case CHARGING_PERIODIC_TASK: 320 taskPeriodMs = /* This flag is in second. */ 321 mddFlags.chargingGcmTaskPeriod() * MILLISECONDS_IN_SECOND; 322 break; 323 case CELLULAR_CHARGING_PERIODIC_TASK: 324 taskPeriodMs = /* This flag is in second. */ 325 mddFlags.cellularChargingGcmTaskPeriod() * MILLISECONDS_IN_SECOND; 326 break; 327 default: 328 taskPeriodMs = /* This flag is in second. */ 329 mddFlags.wifiChargingGcmTaskPeriod() * MILLISECONDS_IN_SECOND; 330 } 331 332 JobInfo job = jobScheduler.getPendingJob(getMddTaskJobId(mddTag)); 333 if (job != null && !forceSchedule) { 334 long scheduledTaskPeriodMs = job.getIntervalMillis(); 335 if (taskPeriodMs == scheduledTaskPeriodMs) { 336 LogUtil.d( 337 "Maintenance Periodic Task has been scheduled with same parameters, skip" 338 + " rescheduling!"); 339 return false; 340 } 341 } 342 343 schedule(context, jobScheduler, taskPeriodMs, mddTag, networkState); 344 return true; 345 } 346 skipAndCancelBackgroundJob( final JobParameters params, int jobId, int skipReason, boolean doRecord)347 private boolean skipAndCancelBackgroundJob( 348 final JobParameters params, int jobId, int skipReason, boolean doRecord) { 349 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 350 if (jobScheduler != null) { 351 jobScheduler.cancel(getMddTaskJobId(getMddTag(params))); 352 } 353 354 if (doRecord) { 355 AdServicesJobServiceLogger.getInstance().recordJobSkipped(jobId, skipReason); 356 } 357 358 // Tell the JobScheduler that the job has completed and does not need to be 359 // rescheduled. 360 jobFinished(params, false); 361 362 // Returning false means that this job has completed its work. 363 return false; 364 } 365 getMddTag(JobParameters params)366 private String getMddTag(JobParameters params) { 367 PersistableBundle extras = params.getExtras(); 368 if (null == extras) { 369 // TODO(b/279231865): Log CEL with SPE_FAIL_TO_FIND_MDD_TASKS_TAG. 370 throw new IllegalArgumentException("Can't find MDD Tasks Tag!"); 371 } 372 return extras.getString(KEY_MDD_TASK_TAG); 373 } 374 375 /** Schedule MDD background tasks. */ 376 @VisibleForTesting schedule( Context context, @NonNull JobScheduler jobScheduler, long jobPeriodMs, @NonNull String mddTag, @NonNull NetworkState networkState)377 static void schedule( 378 Context context, 379 @NonNull JobScheduler jobScheduler, 380 long jobPeriodMs, 381 @NonNull String mddTag, 382 @NonNull NetworkState networkState) { 383 384 // We use Extra to pass the MDD Task Tag. 385 PersistableBundle extras = new PersistableBundle(); 386 extras.putString(KEY_MDD_TASK_TAG, mddTag); 387 388 final JobInfo job = 389 new JobInfo.Builder( 390 getMddTaskJobId(mddTag), 391 new ComponentName(context, MddJobService.class)) 392 .setRequiresCharging(true) 393 .setPersisted(true) 394 .setPeriodic(jobPeriodMs) 395 .setRequiredNetworkType(getNetworkConstraints(networkState)) 396 .setExtras(extras) 397 .build(); 398 399 jobScheduler.schedule(job); 400 LogUtil.d("Scheduling MDD %s job...", mddTag); 401 } 402 403 // Convert from MDD Task Tag to the corresponding JobService ID. getMddTaskJobId(String mddTag)404 private static int getMddTaskJobId(String mddTag) { 405 switch (mddTag) { 406 case MAINTENANCE_PERIODIC_TASK: 407 return MDD_MAINTENANCE_PERIODIC_TASK_JOB.getJobId(); 408 case CHARGING_PERIODIC_TASK: 409 return MDD_CHARGING_PERIODIC_TASK_JOB.getJobId(); 410 case CELLULAR_CHARGING_PERIODIC_TASK: 411 return MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB.getJobId(); 412 default: 413 return MDD_WIFI_CHARGING_PERIODIC_TASK_JOB.getJobId(); 414 } 415 } 416 417 // Maps from the MDD-supplied NetworkState to the JobInfo equivalent int code. getNetworkConstraints(NetworkState networkState)418 private static int getNetworkConstraints(NetworkState networkState) { 419 switch (networkState) { 420 case NETWORK_STATE_ANY: 421 // Network not required. 422 return JobInfo.NETWORK_TYPE_NONE; 423 case NETWORK_STATE_CONNECTED: 424 // Metered or unmetered network available. 425 return JobInfo.NETWORK_TYPE_ANY; 426 case NETWORK_STATE_UNMETERED: 427 default: 428 return JobInfo.NETWORK_TYPE_UNMETERED; 429 } 430 } 431 } 432