1 /*
2  * Copyright (C) 2023 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.measurement.registration;
18 
19 import static com.android.adservices.service.measurement.util.JobLockHolder.Type.ASYNC_REGISTRATION_PROCESSING;
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.MEASUREMENT_ASYNC_REGISTRATION_JOB;
22 
23 import android.app.job.JobInfo;
24 import android.app.job.JobParameters;
25 import android.app.job.JobScheduler;
26 import android.app.job.JobService;
27 import android.content.ComponentName;
28 import android.content.Context;
29 
30 import com.android.adservices.LogUtil;
31 import com.android.adservices.LoggerFactory;
32 import com.android.adservices.concurrency.AdServicesExecutors;
33 import com.android.adservices.service.Flags;
34 import com.android.adservices.service.FlagsFactory;
35 import com.android.adservices.service.common.compat.ServiceCompatUtils;
36 import com.android.adservices.service.measurement.registration.AsyncRegistrationQueueRunner.ProcessingResult;
37 import com.android.adservices.service.measurement.util.JobLockHolder;
38 import com.android.adservices.spe.AdServicesJobServiceLogger;
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 import java.time.Clock;
42 import java.time.Instant;
43 import java.util.concurrent.Future;
44 
45 /** Job Service for servicing queued registration requests */
46 public class AsyncRegistrationQueueJobService extends JobService {
47     private static final int MEASUREMENT_ASYNC_REGISTRATION_JOB_ID =
48             MEASUREMENT_ASYNC_REGISTRATION_JOB.getJobId();
49     private Future mExecutorFuture;
50 
51     @Override
onStartJob(JobParameters params)52     public boolean onStartJob(JobParameters params) {
53         // Always ensure that the first thing this job does is check if it should be running, and
54         // cancel itself if it's not supposed to be.
55         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
56             LogUtil.d(
57                     "Disabling AsyncRegistrationQueueJobService job because it's running in"
58                             + " ExtServices on T+");
59             return skipAndCancelBackgroundJob(params, /* skipReason=*/ 0, /* doRecord=*/ false);
60         }
61 
62         AdServicesJobServiceLogger.getInstance()
63                 .recordOnStartJob(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID);
64 
65         if (FlagsFactory.getFlags().getAsyncRegistrationJobQueueKillSwitch()) {
66             LoggerFactory.getMeasurementLogger().e("AsyncRegistrationQueueJobService is disabled");
67             return skipAndCancelBackgroundJob(
68                     params,
69                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
70                     /* doRecord=*/ true);
71         }
72 
73         Instant jobStartTime = Clock.systemUTC().instant();
74         LoggerFactory.getMeasurementLogger()
75                 .d(
76                         "AsyncRegistrationQueueJobService.onStartJob " + "at %s",
77                         jobStartTime.toString());
78 
79         mExecutorFuture =
80                 AdServicesExecutors.getBlockingExecutor()
81                         .submit(
82                                 () -> {
83                                     ProcessingResult result = processAsyncRecords();
84                                     LoggerFactory.getMeasurementLogger()
85                                             .d(
86                                                     "AsyncRegistrationQueueJobService finished"
87                                                             + " processing [%s]",
88                                                     result);
89 
90                                     final boolean shouldRetry =
91                                             !ProcessingResult.SUCCESS_ALL_RECORDS_PROCESSED.equals(
92                                                     result);
93                                     final boolean isSuccessful =
94                                             !ProcessingResult.THREAD_INTERRUPTED.equals(result);
95                                     AdServicesJobServiceLogger.getInstance()
96                                             .recordJobFinished(
97                                                     MEASUREMENT_ASYNC_REGISTRATION_JOB_ID,
98                                                     isSuccessful,
99                                                     shouldRetry);
100 
101                                     switch (result) {
102                                         case SUCCESS_ALL_RECORDS_PROCESSED:
103                                             // Force scheduling to avoid concurrency issue
104                                             scheduleIfNeeded(this, /* forceSchedule */ true);
105                                             break;
106                                         case SUCCESS_WITH_PENDING_RECORDS:
107                                             scheduleImmediately(
108                                                     AsyncRegistrationQueueJobService.this);
109                                             break;
110                                         case THREAD_INTERRUPTED:
111                                         default:
112                                             // Reschedule with back-off criteria specified when it
113                                             // was
114                                             // scheduled
115                                             jobFinished(params, /* wantsReschedule= */ true);
116                                     }
117                                 });
118         return true;
119     }
120 
121     @VisibleForTesting
processAsyncRecords()122     ProcessingResult processAsyncRecords() {
123         final JobLockHolder lock = JobLockHolder.getInstance(ASYNC_REGISTRATION_PROCESSING);
124         if (lock.tryLock()) {
125             try {
126                 return AsyncRegistrationQueueRunner.getInstance(getApplicationContext())
127                         .runAsyncRegistrationQueueWorker();
128             } finally {
129                 lock.unlock();
130             }
131         }
132         LoggerFactory.getMeasurementLogger()
133                 .d("AsyncRegistrationQueueJobService did not acquire the lock");
134         // Another thread is already processing async registrations.
135         return ProcessingResult.SUCCESS_ALL_RECORDS_PROCESSED;
136     }
137 
138     @Override
onStopJob(JobParameters params)139     public boolean onStopJob(JobParameters params) {
140         LoggerFactory.getMeasurementLogger().d("AsyncRegistrationQueueJobService.onStopJob");
141         boolean shouldRetry = true;
142         if (mExecutorFuture != null) {
143             shouldRetry = mExecutorFuture.cancel(/* mayInterruptIfRunning */ true);
144         }
145         AdServicesJobServiceLogger.getInstance()
146                 .recordOnStopJob(params, MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, shouldRetry);
147         return shouldRetry;
148     }
149 
150     @VisibleForTesting
schedule(JobScheduler jobScheduler, JobInfo job)151     protected static void schedule(JobScheduler jobScheduler, JobInfo job) {
152         jobScheduler.schedule(job);
153     }
154 
buildJobInfo(Context context, Flags flags)155     private static JobInfo buildJobInfo(Context context, Flags flags) {
156         return new JobInfo.Builder(
157                         MEASUREMENT_ASYNC_REGISTRATION_JOB_ID,
158                         new ComponentName(context, AsyncRegistrationQueueJobService.class))
159                 .addTriggerContentUri(
160                         new JobInfo.TriggerContentUri(
161                                 AsyncRegistrationContentProvider.TRIGGER_URI,
162                                 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
163                 .setTriggerContentUpdateDelay(
164                         flags.getMeasurementAsyncRegistrationJobTriggerMinDelayMs())
165                 .setTriggerContentMaxDelay(
166                         flags.getMeasurementAsyncRegistrationJobTriggerMaxDelayMs())
167                 .setRequiredNetworkType(
168                         flags.getMeasurementAsyncRegistrationQueueJobRequiredNetworkType())
169                 // Can't call addTriggerContentUri() on a persisted job
170                 .setPersisted(flags.getMeasurementAsyncRegistrationQueueJobPersisted())
171                 .build();
172     }
173 
174     /**
175      * Schedule Async Registration Queue Job Service if it is not already scheduled
176      *
177      * @param context the context
178      * @param forceSchedule flag to indicate whether to force rescheduling the job.
179      */
scheduleIfNeeded(Context context, boolean forceSchedule)180     public static void scheduleIfNeeded(Context context, boolean forceSchedule) {
181         Flags flags = FlagsFactory.getFlags();
182         if (flags.getAsyncRegistrationJobQueueKillSwitch()) {
183             LoggerFactory.getMeasurementLogger()
184                     .e("AsyncRegistrationQueueJobService is disabled, skip scheduling");
185             return;
186         }
187 
188         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
189         if (jobScheduler == null) {
190             LoggerFactory.getMeasurementLogger().e("JobScheduler not found");
191             return;
192         }
193 
194         final JobInfo scheduledJobInfo =
195                 jobScheduler.getPendingJob(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID);
196         // Schedule if it hasn't been scheduled already or force rescheduling
197         JobInfo jobInfo = buildJobInfo(context, flags);
198         if (forceSchedule || !jobInfo.equals(scheduledJobInfo)) {
199             schedule(jobScheduler, jobInfo);
200             LoggerFactory.getMeasurementLogger().d("Scheduled AsyncRegistrationQueueJobService");
201         } else {
202             LoggerFactory.getMeasurementLogger()
203                     .d("AsyncRegistrationQueueJobService already scheduled, skipping reschedule");
204         }
205     }
206 
207     @VisibleForTesting
scheduleImmediately(Context context)208     void scheduleImmediately(Context context) {
209         Flags flags = FlagsFactory.getFlags();
210         if (flags.getAsyncRegistrationJobQueueKillSwitch()) {
211             LoggerFactory.getMeasurementLogger()
212                     .e("AsyncRegistrationQueueJobService is disabled, skip scheduling");
213             return;
214         }
215 
216         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
217         if (jobScheduler == null) {
218             LoggerFactory.getMeasurementLogger().e("JobScheduler not found");
219             return;
220         }
221 
222         final JobInfo job =
223                 new JobInfo.Builder(
224                                 MEASUREMENT_ASYNC_REGISTRATION_JOB_ID,
225                                 new ComponentName(context, AsyncRegistrationQueueJobService.class))
226                         .setRequiredNetworkType(
227                                 flags.getMeasurementAsyncRegistrationQueueJobRequiredNetworkType())
228                         .build();
229 
230         schedule(jobScheduler, job);
231         LoggerFactory.getMeasurementLogger()
232                 .d("AsyncRegistrationQueueJobService scheduled to run immediately");
233     }
234 
skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)235     private boolean skipAndCancelBackgroundJob(
236             final JobParameters params, int skipReason, boolean doRecord) {
237         final JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
238         if (jobScheduler != null) {
239             jobScheduler.cancel(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID);
240         }
241 
242         if (doRecord) {
243             AdServicesJobServiceLogger.getInstance()
244                     .recordJobSkipped(MEASUREMENT_ASYNC_REGISTRATION_JOB_ID, skipReason);
245         }
246 
247         // Tell the JobScheduler that the job is done and does not need to be rescheduled
248         jobFinished(params, false);
249 
250         // Returning false to reschedule this job.
251         return false;
252     }
253 
254     @VisibleForTesting
getFutureForTesting()255     Future getFutureForTesting() {
256         return mExecutorFuture;
257     }
258 }
259