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.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED;
22 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED;
23 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL;
24 import static com.android.adservices.spe.AdServicesJobInfo.MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB;
25 
26 import android.annotation.RequiresApi;
27 import android.app.job.JobInfo;
28 import android.app.job.JobParameters;
29 import android.app.job.JobScheduler;
30 import android.app.job.JobService;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.os.Build;
34 
35 import com.android.adservices.LogUtil;
36 import com.android.adservices.LoggerFactory;
37 import com.android.adservices.concurrency.AdServicesExecutors;
38 import com.android.adservices.service.Flags;
39 import com.android.adservices.service.FlagsFactory;
40 import com.android.adservices.service.common.compat.ServiceCompatUtils;
41 import com.android.adservices.service.measurement.util.JobLockHolder;
42 import com.android.adservices.shared.common.ApplicationContextSingleton;
43 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode;
44 import com.android.adservices.spe.AdServicesJobServiceLogger;
45 import com.android.internal.annotations.VisibleForTesting;
46 
47 import java.time.Clock;
48 import java.time.Instant;
49 import java.util.concurrent.Future;
50 
51 /** Fallback Job Service for servicing queued registration requests */
52 // TODO(b/328287543): Since Rb has released to R so functionally this class should support R. Due to
53 // Legacy issue, class such as BackgroundJobsManager and MddJobService which have to support R also
54 // have this annotation. It won't have production impact but is needed to bypass the build error.
55 @RequiresApi(Build.VERSION_CODES.S)
56 public class AsyncRegistrationFallbackJobService extends JobService {
57     private static final int MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB_ID =
58             MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB.getJobId();
59 
60     private Future mExecutorFuture;
61 
62     @Override
onStartJob(JobParameters params)63     public boolean onStartJob(JobParameters params) {
64         // Always ensure that the first thing this job does is check if it should be running, and
65         // cancel itself if it's not supposed to be.
66         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
67             LogUtil.d(
68                     "Disabling AsyncRegistrationFallbackJobService job because it's running in"
69                             + " ExtServices on T+");
70             return skipAndCancelBackgroundJob(params, /* skipReason= */ 0, /* doRecord= */ false);
71         }
72 
73         // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this
74         // AsyncRegistrationFallbackJobService will be cancelled for the same job ID.
75         //
76         // Note the job without a flex period will execute immediately after rescheduling with the
77         // same ID. Therefore, ending the execution here and let it run in the new SPE job.
78         if (FlagsFactory.getFlags().getSpeOnAsyncRegistrationFallbackJobEnabled()) {
79             LoggerFactory.getMeasurementLogger()
80                     .d(
81                             "SPE is enabled. Reschedule AsyncRegistrationFallbackJobService with"
82                                     + " AsyncRegistrationFallbackJob.");
83             AsyncRegistrationFallbackJob.schedule();
84             return false;
85         }
86 
87         AdServicesJobServiceLogger.getInstance()
88                 .recordOnStartJob(MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB_ID);
89 
90         if (FlagsFactory.getFlags().getAsyncRegistrationFallbackJobKillSwitch()) {
91             LoggerFactory.getMeasurementLogger()
92                     .e("AsyncRegistrationFallbackJobService is disabled");
93             return skipAndCancelBackgroundJob(
94                     params,
95                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
96                     /* doRecord= */ true);
97         }
98 
99         Instant jobStartTime = Clock.systemUTC().instant();
100         LoggerFactory.getMeasurementLogger()
101                 .d(
102                         "AsyncRegistrationFallbackJobService.onStartJob " + "at %s",
103                         jobStartTime.toString());
104 
105         mExecutorFuture =
106                 AdServicesExecutors.getBlockingExecutor()
107                         .submit(
108                                 () -> {
109                                     processAsyncRecords();
110 
111                                     boolean shouldRetry = false;
112                                     AdServicesJobServiceLogger.getInstance()
113                                             .recordJobFinished(
114                                                     MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB_ID,
115                                                     /* isSuccessful */ true,
116                                                     shouldRetry);
117 
118                                     jobFinished(params, false);
119                                 });
120         return true;
121     }
122 
123     @VisibleForTesting
processAsyncRecords()124     void processAsyncRecords() {
125         final JobLockHolder lock = JobLockHolder.getInstance(ASYNC_REGISTRATION_PROCESSING);
126         if (lock.tryLock()) {
127             try {
128                 AsyncRegistrationQueueRunner.getInstance(getApplicationContext())
129                         .runAsyncRegistrationQueueWorker();
130                 return;
131             } finally {
132                 lock.unlock();
133             }
134         }
135         LoggerFactory.getMeasurementLogger()
136                 .d("AsyncRegistrationFallbackQueueJobService did not acquire the lock");
137     }
138 
139     @Override
onStopJob(JobParameters params)140     public boolean onStopJob(JobParameters params) {
141         LoggerFactory.getMeasurementLogger().d("AsyncRegistrationFallbackJobService.onStopJob");
142         boolean shouldRetry = true;
143         if (mExecutorFuture != null) {
144             shouldRetry = mExecutorFuture.cancel(/* mayInterruptIfRunning */ true);
145         }
146         AdServicesJobServiceLogger.getInstance()
147                 .recordOnStopJob(
148                         params, MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB_ID, shouldRetry);
149         return shouldRetry;
150     }
151 
152     @VisibleForTesting
schedule(JobScheduler jobScheduler, JobInfo jobInfo)153     protected static void schedule(JobScheduler jobScheduler, JobInfo jobInfo) {
154         jobScheduler.schedule(jobInfo);
155     }
156 
157     /**
158      * Schedule Fallback Async Registration Job Service if it is not already scheduled
159      *
160      * @param forceSchedule flag to indicate whether to force rescheduling the job.
161      */
162     @JobSchedulingResultCode
scheduleIfNeeded(boolean forceSchedule)163     public static int scheduleIfNeeded(boolean forceSchedule) {
164         Context context = ApplicationContextSingleton.get();
165         Flags flags = FlagsFactory.getFlags();
166         if (flags.getAsyncRegistrationFallbackJobKillSwitch()) {
167             LoggerFactory.getMeasurementLogger()
168                     .e("AsyncRegistrationFallbackJobService is disabled, skip scheduling");
169             return SCHEDULING_RESULT_CODE_SKIPPED;
170         }
171 
172         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
173         if (jobScheduler == null) {
174             LoggerFactory.getMeasurementLogger().e("JobScheduler not found");
175             return SCHEDULING_RESULT_CODE_FAILED;
176         }
177 
178         final JobInfo scheduledJob =
179                 jobScheduler.getPendingJob(MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB_ID);
180         // Schedule if it hasn't been scheduled already or force rescheduling
181         final JobInfo jobInfo = buildJobInfo(context, flags);
182         if (forceSchedule || !jobInfo.equals(scheduledJob)) {
183             schedule(jobScheduler, jobInfo);
184             LoggerFactory.getMeasurementLogger().d("Scheduled AsyncRegistrationFallbackJobService");
185             return SCHEDULING_RESULT_CODE_SUCCESSFUL;
186         } else {
187             LoggerFactory.getMeasurementLogger()
188                     .d(
189                             "AsyncRegistrationFallbackJobService already scheduled, skipping"
190                                     + " reschedule");
191             return SCHEDULING_RESULT_CODE_SKIPPED;
192         }
193     }
194 
buildJobInfo(Context context, Flags flags)195     private static JobInfo buildJobInfo(Context context, Flags flags) {
196         return new JobInfo.Builder(
197                         MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB_ID,
198                         new ComponentName(context, AsyncRegistrationFallbackJobService.class))
199                 .setRequiresBatteryNotLow(
200                         flags.getMeasurementAsyncRegistrationFallbackJobRequiredBatteryNotLow())
201                 .setPeriodic(flags.getAsyncRegistrationJobQueueIntervalMs())
202                 .setRequiredNetworkType(
203                         flags.getMeasurementAsyncRegistrationFallbackJobRequiredNetworkType())
204                 .setPersisted(flags.getMeasurementAsyncRegistrationFallbackJobPersisted())
205                 .build();
206     }
207 
skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)208     private boolean skipAndCancelBackgroundJob(
209             final JobParameters params, int skipReason, boolean doRecord) {
210         final JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
211         if (jobScheduler != null) {
212             jobScheduler.cancel(MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB_ID);
213         }
214 
215         if (doRecord) {
216             AdServicesJobServiceLogger.getInstance()
217                     .recordJobSkipped(MEASUREMENT_ASYNC_REGISTRATION_FALLBACK_JOB_ID, skipReason);
218         }
219 
220         // Tell the JobScheduler that the job is done and does not need to be rescheduled
221         jobFinished(params, false);
222 
223         // Returning false to reschedule this job.
224         return false;
225     }
226 
227     @VisibleForTesting
getFutureForTesting()228     Future getFutureForTesting() {
229         return mExecutorFuture;
230     }
231 }
232