• 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_KILL_SWITCH_ON;
20 import static com.android.adservices.spe.AdServicesJobInfo.MAINTENANCE_JOB;
21 
22 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
23 
24 import android.annotation.NonNull;
25 import android.app.job.JobInfo;
26 import android.app.job.JobParameters;
27 import android.app.job.JobScheduler;
28 import android.app.job.JobService;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.os.Build;
32 
33 import androidx.annotation.RequiresApi;
34 
35 import com.android.adservices.LogUtil;
36 import com.android.adservices.concurrency.AdServicesExecutors;
37 import com.android.adservices.service.common.FledgeMaintenanceTasksWorker;
38 import com.android.adservices.service.common.compat.ServiceCompatUtils;
39 import com.android.adservices.service.signals.SignalsMaintenanceTasksWorker;
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 @RequiresApi(Build.VERSION_CODES.S)
53 public final class MaintenanceJobService extends JobService {
54     private static final int MAINTENANCE_JOB_ID = MAINTENANCE_JOB.getJobId();
55 
56     private FledgeMaintenanceTasksWorker mFledgeMaintenanceTasksWorker;
57 
58     private SignalsMaintenanceTasksWorker mSignalsMaintenanceTasksWorker;
59 
60     /** Injects a {@link FledgeMaintenanceTasksWorker to be used during testing} */
61     @VisibleForTesting
injectFledgeMaintenanceTasksWorker( @onNull FledgeMaintenanceTasksWorker fledgeMaintenanceTasksWorker)62     public void injectFledgeMaintenanceTasksWorker(
63             @NonNull FledgeMaintenanceTasksWorker fledgeMaintenanceTasksWorker) {
64         mFledgeMaintenanceTasksWorker = fledgeMaintenanceTasksWorker;
65     }
66 
67     /** Injects a {@link SignalsMaintenanceTasksWorker to be used during testing} */
68     @VisibleForTesting
injectSignalsMaintenanceTasksWorker( @onNull SignalsMaintenanceTasksWorker signalsMaintenanceTasksWorker)69     public void injectSignalsMaintenanceTasksWorker(
70             @NonNull SignalsMaintenanceTasksWorker signalsMaintenanceTasksWorker) {
71         mSignalsMaintenanceTasksWorker = signalsMaintenanceTasksWorker;
72     }
73 
74     @Override
onStartJob(JobParameters params)75     public boolean onStartJob(JobParameters params) {
76         // Always ensure that the first thing this job does is check if it should be running, and
77         // cancel itself if it's not supposed to be.
78         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
79             LogUtil.d(
80                     "Disabling MaintenanceJobService job because it's running in ExtServices on"
81                             + " T+");
82             return skipAndCancelBackgroundJob(params, /* skipReason= */ 0, /* doRecord= */ false);
83         }
84 
85         Flags flags = FlagsFactory.getFlags();
86 
87         LogUtil.d("MaintenanceJobService.onStartJob");
88         AdServicesJobServiceLogger.getInstance().recordOnStartJob(MAINTENANCE_JOB_ID);
89 
90         if (flags.getTopicsKillSwitch()
91                 && flags.getFledgeSelectAdsKillSwitch()
92                 && (!flags.getProtectedSignalsCleanupEnabled() || flags.getGlobalKillSwitch())) {
93             LogUtil.e(
94                     "All maintenance jobs are disabled, skipping and cancelling"
95                             + " MaintenanceJobService");
96             return skipAndCancelBackgroundJob(
97                     params,
98                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
99                     /* doRecord= */ true);
100         }
101 
102         ListenableFuture<Boolean> appReconciliationFuture;
103         if (flags.getTopicsKillSwitch()) {
104             LogUtil.d("Topics API is disabled, skipping Topics Job");
105             appReconciliationFuture = Futures.immediateFuture(true);
106         } else {
107             appReconciliationFuture =
108                     submitRunnableAndHandleExceptions(
109                             "Failed to reconcile application update.",
110                             () -> TopicsWorker.getInstance().reconcileApplicationUpdate(this));
111         }
112 
113         ListenableFuture<Boolean> fledgeMaintenanceTasksFuture;
114         if (flags.getFledgeSelectAdsKillSwitch()) {
115             LogUtil.d("Ad Selection API is disabled, skipping Ad Selection Maintenance Job");
116             fledgeMaintenanceTasksFuture = Futures.immediateFuture(true);
117         } else {
118             fledgeMaintenanceTasksFuture =
119                     submitRunnableAndHandleExceptions(
120                             "Failed to complete Ad Selection data maintenance tasks.",
121                             this::doAdSelectionDataMaintenanceTasks);
122         }
123 
124         ListenableFuture<Boolean> protectedSignalsMaintenanceTasksFuture;
125         if (!flags.getProtectedSignalsCleanupEnabled()) {
126             LogUtil.d("Signals cleanup is disabled, skipping maintenance job");
127             protectedSignalsMaintenanceTasksFuture = Futures.immediateFuture(true);
128         } else {
129             protectedSignalsMaintenanceTasksFuture =
130                     submitRunnableAndHandleExceptions(
131                             "Failed to complete Protected Signals data maintenance tasks.",
132                             this::doProtectedSignalsDataMaintenanceTasks);
133         }
134 
135         ListenableFuture<List<Boolean>> futuresList =
136                 Futures.allAsList(
137                         fledgeMaintenanceTasksFuture,
138                         protectedSignalsMaintenanceTasksFuture,
139                         appReconciliationFuture);
140 
141         Futures.addCallback(
142                 futuresList,
143                 new FutureCallback<>() {
144                     @Override
145                     public void onSuccess(List<Boolean> result) {
146                         boolean shouldRetry = false;
147                         boolean isSuccessful = result.stream().allMatch(b -> b);
148 
149                         if (isSuccessful) {
150                             LogUtil.d("PP API jobs are done!");
151                         } else {
152                             LogUtil.e(
153                                     "Failed to handle MaintenanceJobService: " + params.getJobId());
154                         }
155 
156                         AdServicesJobServiceLogger.getInstance()
157                                 .recordJobFinished(MAINTENANCE_JOB_ID, isSuccessful, shouldRetry);
158 
159                         jobFinished(params, shouldRetry);
160                     }
161 
162                     @Override
163                     public void onFailure(Throwable t) {
164                         boolean shouldRetry = false;
165                         AdServicesJobServiceLogger.getInstance()
166                                 .recordJobFinished(
167                                         MAINTENANCE_JOB_ID, /* isSuccessful= */ false, shouldRetry);
168 
169                         LogUtil.e(
170                                 t, "Failed to handle MaintenanceJobService: " + params.getJobId());
171                         jobFinished(params, shouldRetry);
172                     }
173                 },
174                 directExecutor());
175         return true;
176     }
177 
178     @Override
onStopJob(JobParameters params)179     public boolean onStopJob(JobParameters params) {
180         LogUtil.d("MaintenanceJobService.onStopJob");
181 
182         // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the
183         // execution is completed or not to avoid executing the task twice.
184         boolean shouldRetry = false;
185 
186         AdServicesJobServiceLogger.getInstance()
187                 .recordOnStopJob(params, MAINTENANCE_JOB_ID, shouldRetry);
188         return shouldRetry;
189     }
190 
submitRunnableAndHandleExceptions( String message, Runnable runnable)191     private ListenableFuture<Boolean> submitRunnableAndHandleExceptions(
192             String message, Runnable runnable) {
193         return Futures.submit(
194                 () -> {
195                     try {
196                         runnable.run();
197                         return true;
198                     } catch (Exception e) {
199                         LogUtil.e(e, message);
200                         return false;
201                     }
202                 },
203                 AdServicesExecutors.getBackgroundExecutor());
204     }
205 
206     @VisibleForTesting
207     // TODO(b/311183933): Remove passed in Context from static method.
208     @SuppressWarnings("AvoidStaticContext")
209     static void schedule(
210             Context context,
211             @NonNull JobScheduler jobScheduler,
212             long maintenanceJobPeriodMs,
213             long maintenanceJobFlexMs) {
214         final JobInfo job =
215                 new JobInfo.Builder(
216                                 MAINTENANCE_JOB_ID,
217                                 new ComponentName(context, MaintenanceJobService.class))
218                         .setRequiresCharging(true)
219                         .setPersisted(true)
220                         .setPeriodic(maintenanceJobPeriodMs, maintenanceJobFlexMs)
221                         .build();
222 
223         jobScheduler.schedule(job);
224         LogUtil.d("Scheduling maintenance job ...");
225     }
226 
227     /**
228      * Schedule Maintenance Job Service if needed: there is no scheduled job with same job
229      * parameters.
230      *
231      * @param context the context
232      * @param forceSchedule a flag to indicate whether to force rescheduling the job.
233      * @return a {@code boolean} to indicate if the service job is actually scheduled.
234      */
235     // TODO(b/311183933): Remove passed in Context from static method.
236     @SuppressWarnings("AvoidStaticContext")
237     public static boolean scheduleIfNeeded(Context context, boolean forceSchedule) {
238         Flags flags = FlagsFactory.getFlags();
239 
240         if (flags.getTopicsKillSwitch() && flags.getFledgeSelectAdsKillSwitch()) {
241             LogUtil.e(
242                     "Both Topics and Select Ads are disabled, skipping scheduling"
243                             + " MaintenanceJobService");
244             return false;
245         }
246 
247         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
248         if (jobScheduler == null) {
249             LogUtil.e("Cannot fetch Job Scheduler!");
250             return false;
251         }
252 
253         long flagsMaintenanceJobPeriodMs = flags.getMaintenanceJobPeriodMs();
254         long flagsMaintenanceJobFlexMs = flags.getMaintenanceJobFlexMs();
255 
256         JobInfo job = jobScheduler.getPendingJob(MAINTENANCE_JOB_ID);
257         // Skip to reschedule the job if there is same scheduled job with same parameters.
258         if (job != null && !forceSchedule) {
259             long maintenanceJobPeriodMs = job.getIntervalMillis();
260             long maintenanceJobFlexMs = job.getFlexMillis();
261 
262             if (flagsMaintenanceJobPeriodMs == maintenanceJobPeriodMs
263                     && flagsMaintenanceJobFlexMs == maintenanceJobFlexMs) {
264                 LogUtil.d(
265                         "Maintenance Job Service has been scheduled with same parameters, skip"
266                                 + " rescheduling!");
267                 return false;
268             }
269         }
270 
271         schedule(context, jobScheduler, flagsMaintenanceJobPeriodMs, flagsMaintenanceJobFlexMs);
272         return true;
273     }
274 
275     private boolean skipAndCancelBackgroundJob(
276             final JobParameters params, int skipReason, boolean doRecord) {
277         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
278         if (jobScheduler != null) {
279             jobScheduler.cancel(MAINTENANCE_JOB_ID);
280         }
281 
282         if (doRecord) {
283             AdServicesJobServiceLogger.getInstance()
284                     .recordJobSkipped(MAINTENANCE_JOB_ID, skipReason);
285         }
286 
287         // Tell the JobScheduler that the job has completed and does not need to be
288         // rescheduled.
289         jobFinished(params, false);
290 
291         // Returning false means that this job has completed its work.
292         return false;
293     }
294 
295     private FledgeMaintenanceTasksWorker getFledgeMaintenanceTasksWorker() {
296         if (!Objects.isNull(mFledgeMaintenanceTasksWorker)) {
297             return mFledgeMaintenanceTasksWorker;
298         }
299         mFledgeMaintenanceTasksWorker = FledgeMaintenanceTasksWorker.create(this);
300         return mFledgeMaintenanceTasksWorker;
301     }
302 
303     private SignalsMaintenanceTasksWorker getSignalsMaintenanceTasksWorker() {
304         if (!Objects.isNull(mSignalsMaintenanceTasksWorker)) {
305             return mSignalsMaintenanceTasksWorker;
306         }
307         mSignalsMaintenanceTasksWorker = SignalsMaintenanceTasksWorker.create(this);
308         return mSignalsMaintenanceTasksWorker;
309     }
310 
311     private void doAdSelectionDataMaintenanceTasks() {
312         LogUtil.v("Performing Ad Selection maintenance tasks");
313         getFledgeMaintenanceTasksWorker().clearExpiredAdSelectionData();
314         getFledgeMaintenanceTasksWorker()
315                 .clearInvalidFrequencyCapHistogramData(this.getPackageManager());
316         getFledgeMaintenanceTasksWorker().clearExpiredKAnonMessageEntities();
317     }
318 
319     private void doProtectedSignalsDataMaintenanceTasks() {
320         LogUtil.v("Performing protected signals maintenance tasks");
321         getSignalsMaintenanceTasksWorker().clearInvalidProtectedSignalsData();
322     }
323 }
324