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