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