• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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