1 /*
2  * Copyright (C) 2024 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.customaudience;
18 
19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS;
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.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED;
22 import static com.android.adservices.spe.AdServicesJobInfo.SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB;
23 
24 import android.adservices.customaudience.ScheduleCustomAudienceUpdateRequest;
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.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.consent.AdServicesApiType;
42 import com.android.adservices.service.consent.ConsentManager;
43 import com.android.adservices.spe.AdServicesJobServiceLogger;
44 import com.android.internal.annotations.VisibleForTesting;
45 
46 import com.google.common.util.concurrent.FutureCallback;
47 
48 /**
49  * A periodic job that serves as scheduler for handling {@link ScheduleCustomAudienceUpdateRequest}.
50  * When the job is run, it's {@link ScheduleCustomAudienceUpdateWorker} is triggered which initiates
51  * update for pending delayed events.
52  */
53 @RequiresApi(Build.VERSION_CODES.S)
54 public class ScheduleCustomAudienceUpdateJobService extends JobService {
55 
56     private static final int SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID =
57             SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB.getJobId();
58 
59     @Override
onStartJob(JobParameters params)60     public boolean onStartJob(JobParameters params) {
61 
62         // If job is not supposed to be running, cancel itself.
63         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
64             LogUtil.d(
65                     "Disabling ScheduleCustomAudienceUpdate job because it's running in ExtServices"
66                             + " on T+");
67             return skipAndCancelBackgroundJob(
68                     params,
69                     /* skipReason=*/ AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS,
70                     /* doRecord=*/ false);
71         }
72 
73         LoggerFactory.getFledgeLogger().d("ScheduleCustomAudienceUpdateJobService.onStartJob");
74 
75         AdServicesJobServiceLogger.getInstance()
76                 .recordOnStartJob(SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID);
77 
78         if (!FlagsFactory.getFlags().getFledgeScheduleCustomAudienceUpdateEnabled()) {
79             LoggerFactory.getFledgeLogger()
80                     .d(
81                             "FLEDGE Schedule Custom Audience Update API is disabled ; skipping and"
82                                     + " cancelling job");
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         // Skip the execution and cancel the job if user consent is revoked.
90         // Use the per-API consent with GA UX.
91         if (!ConsentManager.getInstance().getConsent(AdServicesApiType.FLEDGE).isGiven()) {
92             LoggerFactory.getFledgeLogger()
93                     .d("User Consent is revoked ; skipping and cancelling job");
94             return skipAndCancelBackgroundJob(
95                     params,
96                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED,
97                     /* doRecord=*/ true);
98         }
99 
100         ScheduleCustomAudienceUpdateWorker updateWorker =
101                 ScheduleCustomAudienceUpdateWorker.getInstance(this);
102         updateWorker
103                 .updateCustomAudience()
104                 .addCallback(
105                         new FutureCallback<Void>() {
106                             @Override
107                             public void onSuccess(Void result) {
108                                 LoggerFactory.getFledgeLogger()
109                                         .d("Schedule Custom Audience Update job completed");
110 
111                                 boolean shouldRetry = false;
112                                 AdServicesJobServiceLogger.getInstance()
113                                         .recordJobFinished(
114                                                 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID,
115                                                 /* isSuccessful= */ true,
116                                                 shouldRetry);
117 
118                                 jobFinished(params, shouldRetry);
119                             }
120 
121                             @Override
122                             public void onFailure(Throwable t) {
123                                 boolean shouldRetry = false;
124                                 LoggerFactory.getFledgeLogger()
125                                         .e(t, "Schedule Custom Audience Update job worker failed");
126                                 AdServicesJobServiceLogger.getInstance()
127                                         .recordJobFinished(
128                                                 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID,
129                                                 /* isSuccessful= */ false,
130                                                 shouldRetry);
131 
132                                 jobFinished(params, shouldRetry);
133                             }
134                         },
135                         AdServicesExecutors.getLightWeightExecutor());
136         return true;
137     }
138 
139     @Override
onStopJob(JobParameters params)140     public boolean onStopJob(JobParameters params) {
141         LoggerFactory.getFledgeLogger().d("ScheduleCustomAudienceUpdateJobService.onStopJob");
142         ScheduleCustomAudienceUpdateWorker.getInstance(this).stopWork();
143 
144         boolean shouldRetry = true;
145         AdServicesJobServiceLogger.getInstance()
146                 .recordOnStopJob(
147                         params, SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID, shouldRetry);
148 
149         return shouldRetry;
150     }
151 
152     /**
153      * Attempts to schedule the update Custom Audience job as a singleton job if it is not already
154      * scheduled.
155      */
scheduleIfNeeded(Context context, Flags flags, boolean forceSchedule)156     public static void scheduleIfNeeded(Context context, Flags flags, boolean forceSchedule) {
157         LoggerFactory.getFledgeLogger()
158                 .v(
159                         "Attempting to schedule job:%s if needed",
160                         SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID);
161 
162         if (!flags.getFledgeScheduleCustomAudienceUpdateEnabled()) {
163             LoggerFactory.getFledgeLogger()
164                     .d(
165                             "FLEDGE Schedule Custom Audience Update API is disabled ; skipping and"
166                                     + " cancelling job");
167             return;
168         }
169 
170         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
171         if ((jobScheduler.getPendingJob(SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID) == null)
172                 || forceSchedule) {
173             schedule(context, flags);
174         } else {
175             LoggerFactory.getFledgeLogger()
176                     .v(
177                             "FLEDGE Schedule Custom Audience Update job already scheduled, skipping"
178                                     + " reschedule");
179         }
180         // TODO(b/267651517) Jobs should be rescheduled if the job-params get updated
181     }
182 
183     /**
184      * Actually schedules the Update Custom Audience job as a singleton job.
185      *
186      * <p>Split out from {@link #scheduleIfNeeded(Context, Flags, boolean)} for mockable testing
187      */
188     @VisibleForTesting
schedule(Context context, Flags flags)189     protected static void schedule(Context context, Flags flags) {
190         if (!flags.getFledgeScheduleCustomAudienceUpdateEnabled()) {
191             LoggerFactory.getFledgeLogger()
192                     .v(
193                             "FLEDGE Schedule Custom Audience Update API is disabled;"
194                                     + " skipping schedule");
195             return;
196         }
197 
198         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
199         final JobInfo job =
200                 new JobInfo.Builder(
201                                 SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID,
202                                 new ComponentName(
203                                         context, ScheduleCustomAudienceUpdateJobService.class))
204                         .setRequiresBatteryNotLow(true)
205                         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
206                         .setPeriodic(
207                                 flags.getFledgeScheduleCustomAudienceUpdateJobPeriodMs(),
208                                 flags.getFledgeScheduleCustomAudienceUpdateJobFlexMs())
209                         .setPersisted(true)
210                         .build();
211         jobScheduler.schedule(job);
212     }
213 
skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)214     private boolean skipAndCancelBackgroundJob(
215             final JobParameters params, int skipReason, boolean doRecord) {
216         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
217         if (jobScheduler != null) {
218             jobScheduler.cancel(SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID);
219         }
220 
221         if (doRecord) {
222             AdServicesJobServiceLogger.getInstance()
223                     .recordJobSkipped(
224                             SCHEDULE_CUSTOM_AUDIENCE_UPDATE_BACKGROUND_JOB_ID, skipReason);
225         }
226 
227         jobFinished(params, false);
228         return false;
229     }
230 }
231