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