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.adselection.encryption;
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.FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB;
23 
24 import android.annotation.RequiresApi;
25 import android.annotation.SuppressLint;
26 import android.app.job.JobInfo;
27 import android.app.job.JobParameters;
28 import android.app.job.JobScheduler;
29 import android.app.job.JobService;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.os.Build;
33 
34 import com.android.adservices.LogUtil;
35 import com.android.adservices.LoggerFactory;
36 import com.android.adservices.concurrency.AdServicesExecutors;
37 import com.android.adservices.service.Flags;
38 import com.android.adservices.service.FlagsFactory;
39 import com.android.adservices.service.common.compat.ServiceCompatUtils;
40 import com.android.adservices.service.consent.AdServicesApiType;
41 import com.android.adservices.service.consent.ConsentManager;
42 import com.android.adservices.spe.AdServicesJobServiceLogger;
43 import com.android.internal.annotations.VisibleForTesting;
44 
45 import com.google.common.util.concurrent.FutureCallback;
46 
47 import java.time.Clock;
48 import java.time.Instant;
49 import java.util.concurrent.ExecutionException;
50 import java.util.concurrent.TimeoutException;
51 
52 /**
53  * Background fetch for Fledge encryption key fetch from the Key Management Servers and periodic
54  * deletion of expired keys.
55  */
56 @SuppressLint("LineLength")
57 @RequiresApi(Build.VERSION_CODES.S)
58 public class BackgroundKeyFetchJobService extends JobService {
59     private static final int FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID =
60             FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB.getJobId();
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 BackgroundKeyFetchJobService job because it's running in "
69                             + " ExtServices on T+");
70             return skipAndCancelKeyFetchJob(
71                     params,
72                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS);
73         }
74 
75         LoggerFactory.getFledgeLogger().d("BackgroundKeyFetchJobService.onStartJob");
76 
77         AdServicesJobServiceLogger.getInstance()
78                 .recordOnStartJob(FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID);
79 
80         if (FlagsFactory.getFlags().getFledgeAuctionServerKillSwitch()) {
81             LoggerFactory.getFledgeLogger()
82                     .d("FLEDGE Ad Selection Data API is disabled ; skipping and cancelling job");
83             return skipAndCancelKeyFetchJob(
84                     params,
85                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
86         }
87 
88         if (!FlagsFactory.getFlags().getFledgeAuctionServerBackgroundKeyFetchJobEnabled()) {
89             LoggerFactory.getFledgeLogger()
90                     .d("FLEDGE background key fetch is disabled; skipping and cancelling job");
91             return skipAndCancelKeyFetchJob(
92                     params,
93                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
94         }
95 
96         // Skip the execution and cancel the job if user consent is revoked.
97         // Use the per-API consent with GA UX.
98         if (!ConsentManager.getInstance().getConsent(AdServicesApiType.FLEDGE).isGiven()) {
99             LoggerFactory.getFledgeLogger()
100                     .d("User Consent is revoked ; skipping and cancelling job");
101             return skipAndCancelKeyFetchJob(
102                     params,
103                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED);
104         }
105 
106         // TODO(b/235841960): Consider using com.android.adservices.service.stats.Clock instead of
107         //  Java Clock
108         Instant jobStartTime = Clock.systemUTC().instant();
109         LoggerFactory.getFledgeLogger()
110                 .d("Starting FLEDGE key fetch job at %s", jobStartTime.toString());
111 
112         BackgroundKeyFetchWorker.getInstance(this)
113                 .runBackgroundKeyFetch()
114                 .addCallback(
115                         new FutureCallback<Void>() {
116                             // Never manually reschedule the background key fetch job, since it is
117                             // already scheduled periodically and should try again as per its
118                             // schedule.
119                             @Override
120                             public void onSuccess(Void result) {
121                                 boolean shouldRetry = false;
122                                 AdServicesJobServiceLogger.getInstance()
123                                         .recordJobFinished(
124                                                 FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID,
125                                                 /* isSuccessful= */ true,
126                                                 shouldRetry);
127 
128                                 jobFinished(params, shouldRetry);
129                             }
130 
131                             @Override
132                             public void onFailure(Throwable t) {
133                                 if (t instanceof InterruptedException) {
134                                     LoggerFactory.getFledgeLogger()
135                                             .e(
136                                                     t,
137                                                     "FLEDGE key background fetch interrupted while"
138                                                             + " waiting for key fetch payload");
139                                 } else if (t instanceof ExecutionException) {
140                                     LoggerFactory.getFledgeLogger()
141                                             .e(
142                                                     t,
143                                                     "FLEDGE key background fetch failed due to"
144                                                             + " internal error");
145                                 } else if (t instanceof TimeoutException) {
146                                     LoggerFactory.getFledgeLogger()
147                                             .e(t, "FLEDGE background key fetch timeout exceeded");
148                                 } else {
149                                     LoggerFactory.getFledgeLogger()
150                                             .e(
151                                                     t,
152                                                     "FLEDGE background key fetch failed due to"
153                                                             + " unexpected error");
154                                 }
155 
156                                 boolean shouldRetry = false;
157                                 AdServicesJobServiceLogger.getInstance()
158                                         .recordJobFinished(
159                                                 FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID,
160                                                 /* isSuccessful= */ false,
161                                                 shouldRetry);
162 
163                                 jobFinished(params, shouldRetry);
164                             }
165                         },
166                         AdServicesExecutors.getLightWeightExecutor());
167 
168         return true;
169     }
170 
171     @Override
onStopJob(JobParameters params)172     public boolean onStopJob(JobParameters params) {
173         LoggerFactory.getFledgeLogger().d("BackgroundKeyFetchJobService.onStopJob");
174         BackgroundKeyFetchWorker.getInstance(this).stopWork();
175 
176         boolean shouldRetry = true;
177 
178         AdServicesJobServiceLogger.getInstance()
179                 .recordOnStopJob(
180                         params, FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID, shouldRetry);
181         return shouldRetry;
182     }
183 
skipAndCancelKeyFetchJob(final JobParameters params, int skipReason)184     private boolean skipAndCancelKeyFetchJob(final JobParameters params, int skipReason) {
185         this.getSystemService(JobScheduler.class)
186                 .cancel(FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID);
187 
188         AdServicesJobServiceLogger.getInstance()
189                 .recordJobSkipped(FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID, skipReason);
190 
191         jobFinished(params, false);
192         return false;
193     }
194 
195     /**
196      * Attempts to schedule the Key Background Fetch as a singleton periodic job if it is not
197      * already scheduled.
198      *
199      * <p>The key fetch background job fetches fresh encryption key, persists them to
200      * EncryptionKeyDb and deletes expired keys.
201      */
scheduleIfNeeded(Context context, Flags flags, boolean forceSchedule)202     public static void scheduleIfNeeded(Context context, Flags flags, boolean forceSchedule) {
203         if (!flags.getFledgeAuctionServerBackgroundKeyFetchJobEnabled()) {
204             LoggerFactory.getFledgeLogger()
205                     .v("Background key fetch is disabled; skipping schedule");
206             return;
207         }
208 
209         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
210 
211         // Scheduling a job can be expensive, and forcing a schedule could interrupt a job that is
212         // already in progress
213         // TODO(b/221837833): Intelligently decide when to overwrite a scheduled job
214         if ((jobScheduler.getPendingJob(FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID) == null)
215                 || forceSchedule) {
216             schedule(context, flags);
217             LoggerFactory.getFledgeLogger().d("Scheduled Background Key Fetch job");
218         } else {
219             LoggerFactory.getFledgeLogger()
220                     .v("Background Key Fetch job already scheduled, skipping reschedule");
221         }
222     }
223 
224     /**
225      * Actually schedules the Background Key Fetch as a singleton periodic job.
226      *
227      * <p>Split out from {@link #scheduleIfNeeded(Context, Flags, boolean)} for mockable testing
228      * without pesky permissions.
229      */
230     @VisibleForTesting
schedule(Context context, Flags flags)231     protected static void schedule(Context context, Flags flags) {
232         if (!flags.getFledgeAuctionServerBackgroundKeyFetchJobEnabled()) {
233             LoggerFactory.getFledgeLogger()
234                     .v("Background key fetch is disabled; skipping schedule");
235             return;
236         }
237 
238         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
239         final JobInfo job =
240                 new JobInfo.Builder(
241                                 FLEDGE_AD_SELECTION_ENCRYPTION_KEY_FETCH_JOB_ID,
242                                 new ComponentName(context, BackgroundKeyFetchJobService.class))
243                         .setRequiresBatteryNotLow(true)
244                         .setRequiresDeviceIdle(true)
245                         .setPeriodic(
246                                 flags.getFledgeAuctionServerBackgroundKeyFetchJobPeriodMs(),
247                                 flags.getFledgeAuctionServerBackgroundKeyFetchJobFlexMs())
248                         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
249                         .setPersisted(true)
250                         .build();
251         jobScheduler.schedule(job);
252     }
253 }
254