• 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.service;
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.MAINTENANCE_JOB;
22 
23 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
24 
25 import android.annotation.NonNull;
26 import android.app.job.JobInfo;
27 import android.app.job.JobParameters;
28 import android.app.job.JobScheduler;
29 import android.app.job.JobService;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.os.Build;
33 
34 import androidx.annotation.RequiresApi;
35 
36 import com.android.adservices.LogUtil;
37 import com.android.adservices.concurrency.AdServicesExecutors;
38 import com.android.adservices.service.common.FledgeMaintenanceTasksWorker;
39 import com.android.adservices.service.common.compat.ServiceCompatUtils;
40 import com.android.adservices.service.topics.TopicsWorker;
41 import com.android.adservices.spe.AdservicesJobServiceLogger;
42 import com.android.internal.annotations.VisibleForTesting;
43 
44 import com.google.common.util.concurrent.FutureCallback;
45 import com.google.common.util.concurrent.Futures;
46 import com.google.common.util.concurrent.ListenableFuture;
47 
48 import java.util.List;
49 import java.util.Objects;
50 
51 /** Maintenance job to clean up. */
52 // TODO(b/269798827): Enable for R.
53 @RequiresApi(Build.VERSION_CODES.S)
54 public final class MaintenanceJobService extends JobService {
55     private static final int MAINTENANCE_JOB_ID = MAINTENANCE_JOB.getJobId();
56 
57     private FledgeMaintenanceTasksWorker mFledgeMaintenanceTasksWorker;
58 
59     /** Injects a {@link FledgeMaintenanceTasksWorker to be used during testing} */
60     @VisibleForTesting
injectFledgeMaintenanceTasksWorker( @onNull FledgeMaintenanceTasksWorker fledgeMaintenanceTasksWorker)61     public void injectFledgeMaintenanceTasksWorker(
62             @NonNull FledgeMaintenanceTasksWorker fledgeMaintenanceTasksWorker) {
63         mFledgeMaintenanceTasksWorker = fledgeMaintenanceTasksWorker;
64     }
65 
66     @Override
onStartJob(JobParameters params)67     public boolean onStartJob(JobParameters params) {
68         // Always ensure that the first thing this job does is check if it should be running, and
69         // cancel itself if it's not supposed to be.
70         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
71             LogUtil.d(
72                     "Disabling MaintenanceJobService job because it's running in ExtServices on"
73                             + " T+");
74             return skipAndCancelBackgroundJob(
75                     params,
76                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS);
77         }
78 
79         LogUtil.d("MaintenanceJobService.onStartJob");
80         AdservicesJobServiceLogger.getInstance(this).recordOnStartJob(MAINTENANCE_JOB_ID);
81 
82         if (FlagsFactory.getFlags().getTopicsKillSwitch()
83                 && FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) {
84             LogUtil.e(
85                     "Both Topics and Select Ads are disabled, skipping and cancelling"
86                             + " MaintenanceJobService");
87             return skipAndCancelBackgroundJob(
88                     params,
89                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
90         }
91 
92         ListenableFuture<Void> appReconciliationFuture;
93         if (FlagsFactory.getFlags().getTopicsKillSwitch()) {
94             LogUtil.d("Topics API is disabled, skipping Topics Job");
95             appReconciliationFuture = Futures.immediateFuture(null);
96         } else {
97             appReconciliationFuture =
98                     Futures.submit(
99                             () -> TopicsWorker.getInstance(this).reconcileApplicationUpdate(this),
100                             AdServicesExecutors.getBackgroundExecutor());
101         }
102 
103         ListenableFuture<Void> fledgeMaintenanceTasksFuture;
104         if (FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) {
105             LogUtil.d("SelectAds API is disabled, skipping SelectAds Job");
106             fledgeMaintenanceTasksFuture = Futures.immediateFuture(null);
107         } else {
108             fledgeMaintenanceTasksFuture =
109                     Futures.submit(
110                             this::doAdSelectionDataMaintenanceTasks,
111                             AdServicesExecutors.getBackgroundExecutor());
112         }
113 
114         ListenableFuture<List<Void>> futuresList =
115                 Futures.allAsList(fledgeMaintenanceTasksFuture, appReconciliationFuture);
116 
117         Futures.addCallback(
118                 futuresList,
119                 new FutureCallback<List<Void>>() {
120                     @Override
121                     public void onSuccess(List<Void> result) {
122                         boolean shouldRetry = false;
123                         AdservicesJobServiceLogger.getInstance(MaintenanceJobService.this)
124                                 .recordJobFinished(
125                                         MAINTENANCE_JOB_ID, /* isSuccessful= */ true, shouldRetry);
126 
127                         LogUtil.d("PP API jobs are done!");
128                         jobFinished(params, shouldRetry);
129                     }
130 
131                     @Override
132                     public void onFailure(Throwable t) {
133                         boolean shouldRetry = false;
134                         AdservicesJobServiceLogger.getInstance(MaintenanceJobService.this)
135                                 .recordJobFinished(
136                                         MAINTENANCE_JOB_ID, /* isSuccessful= */ false, shouldRetry);
137 
138                         LogUtil.e(
139                                 t, "Failed to handle MaintenanceJobService: " + params.getJobId());
140                         jobFinished(params, shouldRetry);
141                     }
142                 },
143                 directExecutor());
144         return true;
145     }
146 
147     @Override
onStopJob(JobParameters params)148     public boolean onStopJob(JobParameters params) {
149         LogUtil.d("MaintenanceJobService.onStopJob");
150 
151         // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the
152         // execution is completed or not to avoid executing the task twice.
153         boolean shouldRetry = false;
154 
155         AdservicesJobServiceLogger.getInstance(this)
156                 .recordOnStopJob(params, MAINTENANCE_JOB_ID, shouldRetry);
157         return shouldRetry;
158     }
159 
160     @VisibleForTesting
schedule( Context context, @NonNull JobScheduler jobScheduler, long maintenanceJobPeriodMs, long maintenanceJobFlexMs)161     static void schedule(
162             Context context,
163             @NonNull JobScheduler jobScheduler,
164             long maintenanceJobPeriodMs,
165             long maintenanceJobFlexMs) {
166         final JobInfo job =
167                 new JobInfo.Builder(
168                                 MAINTENANCE_JOB_ID,
169                                 new ComponentName(context, MaintenanceJobService.class))
170                         .setRequiresCharging(true)
171                         .setPersisted(true)
172                         .setPeriodic(maintenanceJobPeriodMs, maintenanceJobFlexMs)
173                         .build();
174 
175         jobScheduler.schedule(job);
176         LogUtil.d("Scheduling maintenance job ...");
177     }
178 
179     /**
180      * Schedule Maintenance Job Service if needed: there is no scheduled job with same job
181      * parameters.
182      *
183      * @param context the context
184      * @param forceSchedule a flag to indicate whether to force rescheduling the job.
185      * @return a {@code boolean} to indicate if the service job is actually scheduled.
186      */
scheduleIfNeeded(Context context, boolean forceSchedule)187     public static boolean scheduleIfNeeded(Context context, boolean forceSchedule) {
188         if (FlagsFactory.getFlags().getTopicsKillSwitch()
189                 && FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) {
190             LogUtil.e(
191                     "Both Topics and Select Ads are disabled, skipping scheduling"
192                             + " MaintenanceJobService");
193             return false;
194         }
195 
196         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
197         if (jobScheduler == null) {
198             LogUtil.e("Cannot fetch Job Scheduler!");
199             return false;
200         }
201 
202         long flagsMaintenanceJobPeriodMs = FlagsFactory.getFlags().getMaintenanceJobPeriodMs();
203         long flagsMaintenanceJobFlexMs = FlagsFactory.getFlags().getMaintenanceJobFlexMs();
204 
205         JobInfo job = jobScheduler.getPendingJob(MAINTENANCE_JOB_ID);
206         // Skip to reschedule the job if there is same scheduled job with same parameters.
207         if (job != null && !forceSchedule) {
208             long maintenanceJobPeriodMs = job.getIntervalMillis();
209             long maintenanceJobFlexMs = job.getFlexMillis();
210 
211             if (flagsMaintenanceJobPeriodMs == maintenanceJobPeriodMs
212                     && flagsMaintenanceJobFlexMs == maintenanceJobFlexMs) {
213                 LogUtil.i(
214                         "Maintenance Job Service has been scheduled with same parameters, skip"
215                                 + " rescheduling!");
216                 return false;
217             }
218         }
219 
220         schedule(context, jobScheduler, flagsMaintenanceJobPeriodMs, flagsMaintenanceJobFlexMs);
221         return true;
222     }
223 
skipAndCancelBackgroundJob(final JobParameters params, int skipReason)224     private boolean skipAndCancelBackgroundJob(final JobParameters params, int skipReason) {
225         this.getSystemService(JobScheduler.class).cancel(MAINTENANCE_JOB_ID);
226 
227         AdservicesJobServiceLogger.getInstance(this)
228                 .recordJobSkipped(MAINTENANCE_JOB_ID, skipReason);
229 
230         // Tell the JobScheduler that the job has completed and does not need to be
231         // rescheduled.
232         jobFinished(params, false);
233 
234         // Returning false means that this job has completed its work.
235         return false;
236     }
237 
getFledgeMaintenanceTasksWorker()238     private FledgeMaintenanceTasksWorker getFledgeMaintenanceTasksWorker() {
239         if (!Objects.isNull(mFledgeMaintenanceTasksWorker)) {
240             return mFledgeMaintenanceTasksWorker;
241         }
242         mFledgeMaintenanceTasksWorker = FledgeMaintenanceTasksWorker.create(this);
243         return mFledgeMaintenanceTasksWorker;
244     }
245 
doAdSelectionDataMaintenanceTasks()246     private void doAdSelectionDataMaintenanceTasks() {
247         LogUtil.v("Performing Ad Selection maintenance tasks");
248         getFledgeMaintenanceTasksWorker().clearExpiredAdSelectionData();
249     }
250 }
251