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.signals; 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.PERIODIC_SIGNALS_ENCODING_JOB; 23 24 import android.app.job.JobInfo; 25 import android.app.job.JobParameters; 26 import android.app.job.JobScheduler; 27 import android.app.job.JobService; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.os.Build; 31 32 import androidx.annotation.RequiresApi; 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 /** 48 * Periodic encoding background job, periodically encodes raw protected signals into encoded 49 * payloads. Also cleans up stale encoding logic and encoded results. 50 */ 51 @RequiresApi(Build.VERSION_CODES.S) 52 public class PeriodicEncodingJobService extends JobService { 53 54 private static final int PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID = 55 PERIODIC_SIGNALS_ENCODING_JOB.getJobId(); 56 57 @Override onStartJob(JobParameters params)58 public boolean onStartJob(JobParameters params) { 59 60 // If job is not supposed to be running, cancel itself. 61 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 62 LogUtil.d( 63 "Disabling PeriodicEncodingJobService job because it's running in ExtServices" 64 + " on T+"); 65 return skipAndCancelBackgroundJob( 66 params, 67 /* skipReason=*/ AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS, 68 /* doRecord=*/ false); 69 } 70 71 LoggerFactory.getFledgeLogger().d("PeriodicEncodingJobService.onStartJob"); 72 73 AdServicesJobServiceLogger.getInstance() 74 .recordOnStartJob(PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID); 75 76 if (!FlagsFactory.getFlags().getProtectedSignalsPeriodicEncodingEnabled()) { 77 LoggerFactory.getFledgeLogger() 78 .d("FLEDGE periodic encoding is disabled; skipping and cancelling job"); 79 return skipAndCancelBackgroundJob( 80 params, 81 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 82 /* doRecord=*/ true); 83 } 84 85 if (!FlagsFactory.getFlags().getProtectedSignalsEnabled()) { 86 LoggerFactory.getFledgeLogger() 87 .d("FLEDGE Protected Signals API is disabled ; skipping and cancelling job"); 88 return skipAndCancelBackgroundJob( 89 params, 90 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 91 /* doRecord=*/ true); 92 } 93 94 // Skip the execution and cancel the job if user consent is revoked. 95 // Use the per-API consent with GA UX. 96 if (!ConsentManager.getInstance().getConsent(AdServicesApiType.FLEDGE).isGiven()) { 97 LoggerFactory.getFledgeLogger() 98 .d("User Consent is revoked ; skipping and cancelling job"); 99 return skipAndCancelBackgroundJob( 100 params, 101 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED, 102 /* doRecord=*/ true); 103 } 104 105 PeriodicEncodingJobWorker encodingWorker = PeriodicEncodingJobWorker.getInstance(); 106 encodingWorker 107 .encodeProtectedSignals() 108 .addCallback( 109 new FutureCallback<Void>() { 110 @Override 111 public void onSuccess(Void result) { 112 LoggerFactory.getFledgeLogger() 113 .d("PeriodicEncodingJobService encoding completed"); 114 115 boolean shouldRetry = false; 116 AdServicesJobServiceLogger.getInstance() 117 .recordJobFinished( 118 PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID, 119 /* isSuccessful= */ true, 120 shouldRetry); 121 122 jobFinished(params, shouldRetry); 123 } 124 125 @Override 126 public void onFailure(Throwable t) { 127 boolean shouldRetry = false; 128 AdServicesJobServiceLogger.getInstance() 129 .recordJobFinished( 130 PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID, 131 /* isSuccessful= */ false, 132 shouldRetry); 133 134 jobFinished(params, shouldRetry); 135 } 136 }, 137 AdServicesExecutors.getLightWeightExecutor()); 138 return true; 139 } 140 141 @Override onStopJob(JobParameters params)142 public boolean onStopJob(JobParameters params) { 143 LoggerFactory.getFledgeLogger().d("PeriodicEncodingJobService.onStopJob"); 144 PeriodicEncodingJobWorker.getInstance().stopWork(); 145 146 boolean shouldRetry = true; 147 AdServicesJobServiceLogger.getInstance() 148 .recordOnStopJob(params, PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID, shouldRetry); 149 150 return shouldRetry; 151 } 152 153 /** 154 * Attempts to schedule the Periodic encoding as a singleton job if it is not already 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 PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID); 161 if (!flags.getProtectedSignalsPeriodicEncodingEnabled()) { 162 LoggerFactory.getFledgeLogger() 163 .v("Protected Signals periodic encoding is disabled; skipping schedule"); 164 return; 165 } 166 167 if (!flags.getProtectedSignalsEnabled()) { 168 LoggerFactory.getFledgeLogger() 169 .d("FLEDGE Protected Signals API is disabled ; skipping and cancelling job"); 170 return; 171 } 172 173 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 174 JobInfo job = jobScheduler.getPendingJob(PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID); 175 long existingJobPeriodMillis = flags.getProtectedSignalPeriodicEncodingJobPeriodMs(); 176 if (job == null 177 || forceSchedule 178 // Reschedule the job if the period flag has changed 179 || (job.getIntervalMillis() != existingJobPeriodMillis 180 && JobInfo.getMinPeriodMillis() < existingJobPeriodMillis)) { 181 schedule(context, flags); 182 } else { 183 LoggerFactory.getFledgeLogger() 184 .v( 185 "Protected Signals periodic encoding job already scheduled, skipping " 186 + "reschedule"); 187 } 188 } 189 190 /** 191 * Actually schedules the Periodic Encoding as a singleton periodic job. 192 * 193 * <p>Split out from {@link #scheduleIfNeeded(Context, Flags, boolean)} for mockable testing 194 */ 195 @VisibleForTesting schedule(Context context, Flags flags)196 protected static void schedule(Context context, Flags flags) { 197 if (!flags.getProtectedSignalsPeriodicEncodingEnabled()) { 198 LoggerFactory.getFledgeLogger() 199 .v("Protected Signals periodic encoding is disabled; skipping schedule"); 200 return; 201 } 202 203 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 204 final JobInfo job = 205 new JobInfo.Builder( 206 PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID, 207 new ComponentName(context, PeriodicEncodingJobService.class)) 208 .setRequiresBatteryNotLow(true) 209 .setPeriodic( 210 flags.getProtectedSignalPeriodicEncodingJobPeriodMs(), 211 flags.getProtectedSignalsPeriodicEncodingJobFlexMs()) 212 .setPersisted(true) 213 .build(); 214 jobScheduler.schedule(job); 215 } 216 skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)217 private boolean skipAndCancelBackgroundJob( 218 final JobParameters params, int skipReason, boolean doRecord) { 219 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 220 if (jobScheduler != null) { 221 jobScheduler.cancel(PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID); 222 } 223 224 if (doRecord) { 225 AdServicesJobServiceLogger.getInstance() 226 .recordJobSkipped(PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID, skipReason); 227 } 228 229 jobFinished(params, false); 230 return false; 231 } 232 } 233