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