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.measurement.attribution;
18 
19 import static com.android.adservices.service.measurement.util.JobLockHolder.Type.ATTRIBUTION_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_ATTRIBUTION_FALLBACK_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.data.measurement.DatastoreManagerFactory;
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.Trigger;
38 import com.android.adservices.service.measurement.reporting.DebugReportApi;
39 import com.android.adservices.service.measurement.reporting.DebugReportingJobService;
40 import com.android.adservices.service.measurement.reporting.ImmediateAggregateReportingJobService;
41 import com.android.adservices.service.measurement.reporting.ReportingJobService;
42 import com.android.adservices.service.measurement.util.JobLockHolder;
43 import com.android.adservices.spe.AdServicesJobServiceLogger;
44 import com.android.internal.annotations.VisibleForTesting;
45 
46 import com.google.common.util.concurrent.ListeningExecutorService;
47 
48 import java.util.concurrent.Future;
49 
50 /**
51  * Fallback attribution job. The actual job execution logic is part of {@link
52  * AttributionJobHandler}.
53  */
54 public class AttributionFallbackJobService extends JobService {
55     private static final int MEASUREMENT_ATTRIBUTION_FALLBACK_JOB_ID =
56             MEASUREMENT_ATTRIBUTION_FALLBACK_JOB.getJobId();
57     private static final ListeningExecutorService sBackgroundExecutor =
58             AdServicesExecutors.getBackgroundExecutor();
59     private Future mExecutorFuture;
60 
61     @Override
onCreate()62     public void onCreate() {
63         LogUtil.d("AttributionFallbackJobService.onCreate");
64         super.onCreate();
65     }
66 
67     @Override
onStartJob(JobParameters params)68     public boolean onStartJob(JobParameters params) {
69         // Always ensure that the first thing this job does is check if it should be running, and
70         // cancel itself if it's not supposed to be.
71         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
72             LogUtil.d(
73                     "Disabling AttributionFallbackJobService job because it's running in"
74                             + " ExtServices on T+");
75             return skipAndCancelBackgroundJob(params, /* skipReason=*/ 0, /* doRecord=*/ false);
76         }
77 
78         AdServicesJobServiceLogger.getInstance()
79                 .recordOnStartJob(MEASUREMENT_ATTRIBUTION_FALLBACK_JOB_ID);
80 
81         if (!FlagsFactory.getFlags().getMeasurementAttributionFallbackJobEnabled()) {
82             LoggerFactory.getMeasurementLogger().e("AttributionFallbackJobService is disabled");
83             return skipAndCancelBackgroundJob(
84                     params,
85                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
86                     /* doRecord=*/ true);
87         }
88 
89         LoggerFactory.getMeasurementLogger().d("AttributionFallbackJobService.onStartJob");
90         mExecutorFuture =
91                 sBackgroundExecutor.submit(
92                         () -> {
93                             processPendingAttributions();
94 
95                             DebugReportingJobService.scheduleIfNeeded(
96                                     getApplicationContext(), /* forceSchedule */ false);
97 
98                             // TODO(b/342687685): fold this service into ReportingJobService
99                             ImmediateAggregateReportingJobService.scheduleIfNeeded(
100                                     getApplicationContext(), /* forceSchedule */ false);
101 
102                             ReportingJobService.scheduleIfNeeded(
103                                     getApplicationContext(), /* forceSchedule */ false);
104 
105                             AdServicesJobServiceLogger.getInstance()
106                                     .recordJobFinished(
107                                             MEASUREMENT_ATTRIBUTION_FALLBACK_JOB_ID,
108                                             /* isSuccessful */ true,
109                                             /* shouldRetry */ false);
110 
111                             jobFinished(params, /* wantsReschedule= */ false);
112                         });
113         return true;
114     }
115 
116     @VisibleForTesting
processPendingAttributions()117     void processPendingAttributions() {
118         final JobLockHolder lock = JobLockHolder.getInstance(ATTRIBUTION_PROCESSING);
119         if (lock.tryLock()) {
120             try {
121                 new AttributionJobHandler(
122                                 DatastoreManagerFactory.getDatastoreManager(
123                                         getApplicationContext()),
124                                 new DebugReportApi(
125                                         getApplicationContext(), FlagsFactory.getFlags()))
126                         .performPendingAttributions();
127                 return;
128             } finally {
129                 lock.unlock();
130             }
131         }
132         LoggerFactory.getMeasurementLogger()
133                 .d("AttributionFallbackJobService did not acquire the lock");
134     }
135 
136     @Override
onStopJob(JobParameters params)137     public boolean onStopJob(JobParameters params) {
138         LoggerFactory.getMeasurementLogger().d("AttributionFallbackJobService.onStopJob");
139         boolean shouldRetry = true;
140         if (mExecutorFuture != null) {
141             shouldRetry = mExecutorFuture.cancel(/* mayInterruptIfRunning */ true);
142         }
143         AdServicesJobServiceLogger.getInstance()
144                 .recordOnStopJob(params, MEASUREMENT_ATTRIBUTION_FALLBACK_JOB_ID, shouldRetry);
145         return shouldRetry;
146     }
147 
148     /**
149      * Schedules {@link AttributionFallbackJobService} to observer {@link Trigger} content URI
150      * change.
151      */
152     @VisibleForTesting
schedule(JobScheduler jobScheduler, JobInfo job)153     static void schedule(JobScheduler jobScheduler, JobInfo job) {
154         jobScheduler.schedule(job);
155     }
156 
buildJobInfo(Context context, Flags flags)157     private static JobInfo buildJobInfo(Context context, Flags flags) {
158         return new JobInfo.Builder(
159                         MEASUREMENT_ATTRIBUTION_FALLBACK_JOB_ID,
160                         new ComponentName(context, AttributionFallbackJobService.class))
161                 .setPeriodic(flags.getMeasurementAttributionFallbackJobPeriodMs())
162                 .setPersisted(flags.getMeasurementAttributionFallbackJobPersisted())
163                 .build();
164     }
165 
166     /**
167      * Schedule Attribution Fallback Job if it is not already scheduled
168      *
169      * @param context the context
170      * @param forceSchedule flag to indicate whether to force rescheduling the job.
171      */
scheduleIfNeeded(Context context, boolean forceSchedule)172     public static void scheduleIfNeeded(Context context, boolean forceSchedule) {
173         Flags flags = FlagsFactory.getFlags();
174         if (!flags.getMeasurementAttributionFallbackJobEnabled()) {
175             LoggerFactory.getMeasurementLogger()
176                     .e("AttributionFallbackJobService is disabled, skip scheduling");
177             return;
178         }
179 
180         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
181         if (jobScheduler == null) {
182             LoggerFactory.getMeasurementLogger().e("JobScheduler not found");
183             return;
184         }
185 
186         final JobInfo scheduledJob =
187                 jobScheduler.getPendingJob(MEASUREMENT_ATTRIBUTION_FALLBACK_JOB_ID);
188         // Schedule if it hasn't been scheduled already or force rescheduling
189         JobInfo jobInfo = buildJobInfo(context, flags);
190         if (forceSchedule || !jobInfo.equals(scheduledJob)) {
191             schedule(jobScheduler, jobInfo);
192             LoggerFactory.getMeasurementLogger().d("Scheduled AttributionFallbackJobService");
193         } else {
194             LoggerFactory.getMeasurementLogger()
195                     .d("AttributionFallbackJobService already scheduled, skipping reschedule");
196         }
197     }
198 
skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)199     private boolean skipAndCancelBackgroundJob(
200             final JobParameters params, int skipReason, boolean doRecord) {
201         final JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
202         if (jobScheduler != null) {
203             jobScheduler.cancel(MEASUREMENT_ATTRIBUTION_FALLBACK_JOB_ID);
204         }
205 
206         if (doRecord) {
207             AdServicesJobServiceLogger.getInstance()
208                     .recordJobSkipped(MEASUREMENT_ATTRIBUTION_FALLBACK_JOB_ID, skipReason);
209         }
210 
211         // Tell the JobScheduler that the job has completed and does not need to be rescheduled.
212         jobFinished(params, false);
213 
214         // Returning false means that this job has completed its work.
215         return false;
216     }
217 
218     @VisibleForTesting
getFutureForTesting()219     Future getFutureForTesting() {
220         return mExecutorFuture;
221     }
222 }
223