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.customaudience;
18 
19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON;
20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED;
21 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED;
22 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL;
23 import static com.android.adservices.spe.AdServicesJobInfo.FLEDGE_BACKGROUND_FETCH_JOB;
24 
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.shared.common.ApplicationContextSingleton;
44 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode;
45 import com.android.adservices.spe.AdServicesJobServiceLogger;
46 import com.android.internal.annotations.VisibleForTesting;
47 
48 import com.google.common.util.concurrent.FutureCallback;
49 
50 import java.time.Clock;
51 import java.time.Instant;
52 import java.util.concurrent.ExecutionException;
53 import java.util.concurrent.TimeoutException;
54 
55 /**
56  * Background fetch for FLEDGE Custom Audience API, executing periodic garbage collection and custom
57  * audience updates.
58  */
59 // TODO(b/269798827): Enable for R.
60 @RequiresApi(Build.VERSION_CODES.S)
61 public class BackgroundFetchJobService extends JobService {
62     private static final int FLEDGE_BACKGROUND_FETCH_JOB_ID =
63             FLEDGE_BACKGROUND_FETCH_JOB.getJobId();
64 
65     @Override
onStartJob(JobParameters params)66     public boolean onStartJob(JobParameters params) {
67         // Always ensure that the first thing this job does is check if it should be running, and
68         // cancel itself if it's not supposed to be.
69         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
70             LogUtil.d(
71                     "Disabling BackgroundFetchJobService job because it's running in ExtServices"
72                             + " on T+");
73             return skipAndCancelBackgroundJob(params, /* skipReason= */ 0, /* doRecord= */ false);
74         }
75 
76         LoggerFactory.getFledgeLogger().d("BackgroundFetchJobService.onStartJob");
77 
78         AdServicesJobServiceLogger.getInstance().recordOnStartJob(FLEDGE_BACKGROUND_FETCH_JOB_ID);
79 
80         Flags flags = FlagsFactory.getFlags();
81         if (!flags.getFledgeBackgroundFetchEnabled()) {
82             LoggerFactory.getFledgeLogger()
83                     .d("FLEDGE background fetch is disabled; skipping and cancelling job");
84             return skipAndCancelBackgroundJob(
85                     params,
86                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
87                     /* doRecord= */ true);
88         }
89 
90         if (flags.getFledgeCustomAudienceServiceKillSwitch()) {
91             LoggerFactory.getFledgeLogger()
92                     .d("FLEDGE Custom Audience API is disabled ; skipping and cancelling job");
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         // Skip the execution and cancel the job if user consent is revoked.
100         // Use the per-API consent with GA UX.
101         if (!ConsentManager.getInstance().getConsent(AdServicesApiType.FLEDGE).isGiven()) {
102             LoggerFactory.getFledgeLogger()
103                     .d("User Consent is revoked ; skipping and cancelling job");
104             return skipAndCancelBackgroundJob(
105                     params,
106                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED,
107                     /* doRecord= */ true);
108         }
109 
110         // TODO(b/235841960): Consider using com.android.adservices.service.stats.Clock instead of
111         //  Java Clock
112         Instant jobStartTime = Clock.systemUTC().instant();
113         LoggerFactory.getFledgeLogger()
114                 .d("Starting FLEDGE background fetch job at %s", jobStartTime.toString());
115 
116         BackgroundFetchWorker.getInstance(this)
117                 .runBackgroundFetch()
118                 .addCallback(
119                         new FutureCallback<Void>() {
120                             // Never manually reschedule the background fetch job, since it is
121                             // already scheduled periodically and should try again multiple times
122                             // per day
123                             @Override
124                             public void onSuccess(Void result) {
125                                 boolean shouldRetry = false;
126                                 AdServicesJobServiceLogger.getInstance()
127                                         .recordJobFinished(
128                                                 FLEDGE_BACKGROUND_FETCH_JOB_ID,
129                                                 /* isSuccessful= */ true,
130                                                 shouldRetry);
131 
132                                 jobFinished(params, shouldRetry);
133                             }
134 
135                             @Override
136                             public void onFailure(Throwable t) {
137                                 if (t instanceof InterruptedException) {
138                                     LoggerFactory.getFledgeLogger()
139                                             .e(
140                                                     t,
141                                                     "FLEDGE background fetch interrupted while"
142                                                         + " waiting for custom audience updates");
143                                 } else if (t instanceof ExecutionException) {
144                                     LoggerFactory.getFledgeLogger()
145                                             .e(
146                                                     t,
147                                                     "FLEDGE background fetch failed due to"
148                                                             + " internal error");
149                                 } else if (t instanceof TimeoutException) {
150                                     LoggerFactory.getFledgeLogger()
151                                             .e(t, "FLEDGE background fetch timeout exceeded");
152                                 } else {
153                                     LoggerFactory.getFledgeLogger()
154                                             .e(
155                                                     t,
156                                                     "FLEDGE background fetch failed due to"
157                                                             + " unexpected error");
158                                 }
159 
160                                 boolean shouldRetry = false;
161                                 AdServicesJobServiceLogger.getInstance()
162                                         .recordJobFinished(
163                                                 FLEDGE_BACKGROUND_FETCH_JOB_ID,
164                                                 /* isSuccessful= */ false,
165                                                 shouldRetry);
166 
167                                 jobFinished(params, shouldRetry);
168                             }
169                         },
170                         AdServicesExecutors.getLightWeightExecutor());
171 
172         // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this
173         // BackgroundFetchJobService will be cancelled for the same job ID.
174         //
175         // Also for a job with Flex Period, it will NOT execute immediately after rescheduling it.
176         // Reschedule it here to let the execution complete and the next cycle will execute with
177         // the BackgroundFetchJob.schedule().
178         if (flags.getSpeOnBackgroundFetchJobEnabled()) {
179             LoggerFactory.getFledgeLogger()
180                     .d("SPE is enabled. Reschedule BackgroundFetchJobService with SPE framework.");
181             BackgroundFetchJob.schedule(flags);
182         }
183 
184         return true;
185     }
186 
skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)187     private boolean skipAndCancelBackgroundJob(
188             final JobParameters params, int skipReason, boolean doRecord) {
189         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
190         if (jobScheduler != null) {
191             jobScheduler.cancel(FLEDGE_BACKGROUND_FETCH_JOB_ID);
192         }
193 
194         if (doRecord) {
195             AdServicesJobServiceLogger.getInstance()
196                     .recordJobSkipped(FLEDGE_BACKGROUND_FETCH_JOB_ID, skipReason);
197         }
198 
199         jobFinished(params, false);
200         return false;
201     }
202 
203     @Override
onStopJob(JobParameters params)204     public boolean onStopJob(JobParameters params) {
205         LoggerFactory.getFledgeLogger().d("BackgroundFetchJobService.onStopJob");
206         BackgroundFetchWorker.getInstance(this).stopWork();
207 
208         boolean shouldRetry = true;
209 
210         AdServicesJobServiceLogger.getInstance()
211                 .recordOnStopJob(params, FLEDGE_BACKGROUND_FETCH_JOB_ID, shouldRetry);
212         return shouldRetry;
213     }
214 
215     /**
216      * Attempts to schedule the FLEDGE Background Fetch as a singleton periodic job if it is not
217      * already scheduled.
218      *
219      * <p>The background fetch primarily updates custom audiences' ads and bidding data. It also
220      * prunes the custom audience database of any expired data.
221      */
222     @JobSchedulingResultCode
scheduleIfNeeded(Flags flags, boolean forceSchedule)223     public static int scheduleIfNeeded(Flags flags, boolean forceSchedule) {
224         Context context = ApplicationContextSingleton.get();
225         if (!flags.getFledgeBackgroundFetchEnabled()) {
226             LoggerFactory.getFledgeLogger()
227                     .v("FLEDGE background fetch is disabled; skipping schedule");
228             return SCHEDULING_RESULT_CODE_SKIPPED;
229         }
230 
231         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
232 
233         // Scheduling a job can be expensive, and forcing a schedule could interrupt a job that is
234         // already in progress
235         // TODO(b/221837833): Intelligently decide when to overwrite a scheduled job
236         if ((jobScheduler.getPendingJob(FLEDGE_BACKGROUND_FETCH_JOB_ID) == null) || forceSchedule) {
237             schedule(context, flags);
238             LoggerFactory.getFledgeLogger().d("Scheduled FLEDGE Background Fetch job");
239             return SCHEDULING_RESULT_CODE_SUCCESSFUL;
240         } else {
241             LoggerFactory.getFledgeLogger()
242                     .v("FLEDGE Background Fetch job already scheduled, skipping reschedule");
243             return SCHEDULING_RESULT_CODE_SKIPPED;
244         }
245     }
246 
247     /**
248      * Actually schedules the FLEDGE Background Fetch as a singleton periodic job.
249      *
250      * <p>Split out from {@link #scheduleIfNeeded(Flags, boolean)} for mockable testing without
251      * pesky permissions.
252      */
253     @VisibleForTesting
schedule(Context context, Flags flags)254     protected static void schedule(Context context, Flags flags) {
255         if (!flags.getFledgeBackgroundFetchEnabled()) {
256             LoggerFactory.getFledgeLogger()
257                     .v("FLEDGE background fetch is disabled; skipping schedule");
258             return;
259         }
260 
261         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
262         final JobInfo job =
263                 new JobInfo.Builder(
264                                 FLEDGE_BACKGROUND_FETCH_JOB_ID,
265                                 new ComponentName(context, BackgroundFetchJobService.class))
266                         .setRequiresBatteryNotLow(true)
267                         .setRequiresDeviceIdle(true)
268                         .setPeriodic(
269                                 flags.getFledgeBackgroundFetchJobPeriodMs(),
270                                 flags.getFledgeBackgroundFetchJobFlexMs())
271                         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
272                         .setPersisted(true)
273                         .build();
274         jobScheduler.schedule(job);
275     }
276 }
277