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<Void> appReconciliationFuture;
103         if (flags.getTopicsKillSwitch()) {
104             LogUtil.d("Topics API is disabled, skipping Topics Job");
105             appReconciliationFuture = Futures.immediateFuture(null);
106         } else {
107             appReconciliationFuture =
108                     Futures.submit(
109                             () -> TopicsWorker.getInstance().reconcileApplicationUpdate(this),
110                             AdServicesExecutors.getBackgroundExecutor());
111         }
112 
113         ListenableFuture<Void> fledgeMaintenanceTasksFuture;
114         if (flags.getFledgeSelectAdsKillSwitch()) {
115             LogUtil.d("Ad Selection API is disabled, skipping Ad Selection Maintenance Job");
116             fledgeMaintenanceTasksFuture = Futures.immediateFuture(null);
117         } else {
118             fledgeMaintenanceTasksFuture =
119                     Futures.submit(
120                             this::doAdSelectionDataMaintenanceTasks,
121                             AdServicesExecutors.getBackgroundExecutor());
122         }
123 
124         ListenableFuture<Void> protectedSignalsMaintenanceTasksFuture;
125         if (!flags.getProtectedSignalsCleanupEnabled()) {
126             LogUtil.d("Signals cleanup is disabled, skipping maintenance job");
127             protectedSignalsMaintenanceTasksFuture = Futures.immediateFuture(null);
128         } else {
129             protectedSignalsMaintenanceTasksFuture =
130                     Futures.submit(
131                             this::doProtectedSignalsDataMaintenanceTasks,
132                             AdServicesExecutors.getBackgroundExecutor());
133         }
134 
135         ListenableFuture<List<Void>> futuresList =
136                 Futures.allAsList(
137                         fledgeMaintenanceTasksFuture,
138                         protectedSignalsMaintenanceTasksFuture,
139                         appReconciliationFuture);
140 
141         Futures.addCallback(
142                 futuresList,
143                 new FutureCallback<List<Void>>() {
144                     @Override
145                     public void onSuccess(List<Void> result) {
146                         boolean shouldRetry = false;
147                         AdServicesJobServiceLogger.getInstance()
148                                 .recordJobFinished(
149                                         MAINTENANCE_JOB_ID, /* isSuccessful= */ true, shouldRetry);
150 
151                         LogUtil.d("PP API jobs are done!");
152                         jobFinished(params, shouldRetry);
153                     }
154 
155                     @Override
156                     public void onFailure(Throwable t) {
157                         boolean shouldRetry = false;
158                         AdServicesJobServiceLogger.getInstance()
159                                 .recordJobFinished(
160                                         MAINTENANCE_JOB_ID, /* isSuccessful= */ false, shouldRetry);
161 
162                         LogUtil.e(
163                                 t, "Failed to handle MaintenanceJobService: " + params.getJobId());
164                         jobFinished(params, shouldRetry);
165                     }
166                 },
167                 directExecutor());
168         return true;
169     }
170 
171     @Override
onStopJob(JobParameters params)172     public boolean onStopJob(JobParameters params) {
173         LogUtil.d("MaintenanceJobService.onStopJob");
174 
175         // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the
176         // execution is completed or not to avoid executing the task twice.
177         boolean shouldRetry = false;
178 
179         AdServicesJobServiceLogger.getInstance()
180                 .recordOnStopJob(params, MAINTENANCE_JOB_ID, shouldRetry);
181         return shouldRetry;
182     }
183 
184     @VisibleForTesting
schedule( Context context, @NonNull JobScheduler jobScheduler, long maintenanceJobPeriodMs, long maintenanceJobFlexMs)185     static void schedule(
186             Context context,
187             @NonNull JobScheduler jobScheduler,
188             long maintenanceJobPeriodMs,
189             long maintenanceJobFlexMs) {
190         final JobInfo job =
191                 new JobInfo.Builder(
192                                 MAINTENANCE_JOB_ID,
193                                 new ComponentName(context, MaintenanceJobService.class))
194                         .setRequiresCharging(true)
195                         .setPersisted(true)
196                         .setPeriodic(maintenanceJobPeriodMs, maintenanceJobFlexMs)
197                         .build();
198 
199         jobScheduler.schedule(job);
200         LogUtil.d("Scheduling maintenance job ...");
201     }
202 
203     /**
204      * Schedule Maintenance Job Service if needed: there is no scheduled job with same job
205      * parameters.
206      *
207      * @param context the context
208      * @param forceSchedule a flag to indicate whether to force rescheduling the job.
209      * @return a {@code boolean} to indicate if the service job is actually scheduled.
210      */
scheduleIfNeeded(Context context, boolean forceSchedule)211     public static boolean scheduleIfNeeded(Context context, boolean forceSchedule) {
212         Flags flags = FlagsFactory.getFlags();
213 
214         if (flags.getTopicsKillSwitch() && flags.getFledgeSelectAdsKillSwitch()) {
215             LogUtil.e(
216                     "Both Topics and Select Ads are disabled, skipping scheduling"
217                             + " MaintenanceJobService");
218             return false;
219         }
220 
221         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
222         if (jobScheduler == null) {
223             LogUtil.e("Cannot fetch Job Scheduler!");
224             return false;
225         }
226 
227         long flagsMaintenanceJobPeriodMs = flags.getMaintenanceJobPeriodMs();
228         long flagsMaintenanceJobFlexMs = flags.getMaintenanceJobFlexMs();
229 
230         JobInfo job = jobScheduler.getPendingJob(MAINTENANCE_JOB_ID);
231         // Skip to reschedule the job if there is same scheduled job with same parameters.
232         if (job != null && !forceSchedule) {
233             long maintenanceJobPeriodMs = job.getIntervalMillis();
234             long maintenanceJobFlexMs = job.getFlexMillis();
235 
236             if (flagsMaintenanceJobPeriodMs == maintenanceJobPeriodMs
237                     && flagsMaintenanceJobFlexMs == maintenanceJobFlexMs) {
238                 LogUtil.d(
239                         "Maintenance Job Service has been scheduled with same parameters, skip"
240                                 + " rescheduling!");
241                 return false;
242             }
243         }
244 
245         schedule(context, jobScheduler, flagsMaintenanceJobPeriodMs, flagsMaintenanceJobFlexMs);
246         return true;
247     }
248 
skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)249     private boolean skipAndCancelBackgroundJob(
250             final JobParameters params, int skipReason, boolean doRecord) {
251         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
252         if (jobScheduler != null) {
253             jobScheduler.cancel(MAINTENANCE_JOB_ID);
254         }
255 
256         if (doRecord) {
257             AdServicesJobServiceLogger.getInstance()
258                     .recordJobSkipped(MAINTENANCE_JOB_ID, skipReason);
259         }
260 
261         // Tell the JobScheduler that the job has completed and does not need to be
262         // rescheduled.
263         jobFinished(params, false);
264 
265         // Returning false means that this job has completed its work.
266         return false;
267     }
268 
getFledgeMaintenanceTasksWorker()269     private FledgeMaintenanceTasksWorker getFledgeMaintenanceTasksWorker() {
270         if (!Objects.isNull(mFledgeMaintenanceTasksWorker)) {
271             return mFledgeMaintenanceTasksWorker;
272         }
273         mFledgeMaintenanceTasksWorker = FledgeMaintenanceTasksWorker.create(this);
274         return mFledgeMaintenanceTasksWorker;
275     }
276 
getSignalsMaintenanceTasksWorker()277     private SignalsMaintenanceTasksWorker getSignalsMaintenanceTasksWorker() {
278         if (!Objects.isNull(mSignalsMaintenanceTasksWorker)) {
279             return mSignalsMaintenanceTasksWorker;
280         }
281         mSignalsMaintenanceTasksWorker = SignalsMaintenanceTasksWorker.create(this);
282         return mSignalsMaintenanceTasksWorker;
283     }
284 
doAdSelectionDataMaintenanceTasks()285     private void doAdSelectionDataMaintenanceTasks() {
286         LogUtil.v("Performing Ad Selection maintenance tasks");
287         getFledgeMaintenanceTasksWorker().clearExpiredAdSelectionData();
288         getFledgeMaintenanceTasksWorker()
289                 .clearInvalidFrequencyCapHistogramData(this.getPackageManager());
290         getFledgeMaintenanceTasksWorker().clearExpiredKAnonMessageEntities();
291     }
292 
doProtectedSignalsDataMaintenanceTasks()293     private void doProtectedSignalsDataMaintenanceTasks() {
294         LogUtil.v("Performing protected signals maintenance tasks");
295         getSignalsMaintenanceTasksWorker().clearInvalidProtectedSignalsData();
296     }
297 }
298