• 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_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