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.cobalt; 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.spe.AdServicesJobInfo.COBALT_LOGGING_JOB; 21 22 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 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 import androidx.annotation.VisibleForTesting; 34 35 import com.android.adservices.LogUtil; 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.spe.AdServicesJobServiceLogger; 41 42 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 43 import com.google.common.util.concurrent.FutureCallback; 44 import com.google.common.util.concurrent.Futures; 45 import com.google.common.util.concurrent.ListenableFuture; 46 47 /** 48 * Cobalt JobService. This will trigger cobalt generate observation and upload logging in background 49 * tasks. 50 */ 51 @RequiresApi(Build.VERSION_CODES.S) 52 public final class CobaltJobService extends JobService { 53 private static final int COBALT_LOGGING_JOB_ID = COBALT_LOGGING_JOB.getJobId(); 54 55 @Override onStartJob(JobParameters params)56 public boolean onStartJob(JobParameters params) { 57 // Always ensure that the first thing this job does is check if it should be running, and 58 // cancel itself if it's not supposed to be. 59 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 60 LogUtil.d("Disabling cobalt logging job because it's running in ExtServices on T+"); 61 // Do not log via the AdservicesJobServiceLogger because the it might cause 62 // ClassNotFound exception on earlier beta versions. 63 return skipAndCancelBackgroundJob( 64 params, COBALT_LOGGING_JOB_ID, /* skipReason= */ 0, /* doRecord= */ false); 65 } 66 67 Flags flags = FlagsFactory.getFlags(); 68 69 // Record the invocation of onStartJob() for logging purpose. 70 LogUtil.d("CobaltJobService.onStartJob"); 71 AdServicesJobServiceLogger.getInstance().recordOnStartJob(COBALT_LOGGING_JOB_ID); 72 73 if (!flags.getCobaltLoggingEnabled()) { 74 LogUtil.d( 75 "Cobalt logging killswitch is enabled, skipping and cancelling" 76 + " CobaltJobService"); 77 return skipAndCancelBackgroundJob( 78 params, 79 COBALT_LOGGING_JOB_ID, 80 /* skipReason= */ AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, 81 /* doRecord= */ true); 82 } 83 84 ListenableFuture<Void> cobaltLoggingFuture = 85 PropagatedFutures.submitAsync( 86 () -> { 87 LogUtil.d("CobaltJobService.onStart Job."); 88 return CobaltFactory.getCobaltPeriodicJob(this, flags) 89 .generateAggregatedObservations(); 90 }, 91 AdServicesExecutors.getBackgroundExecutor()); 92 93 // Background job logging in onSuccess and OnFailure have to happen before jobFinished() is 94 // called. Due to JobScheduler infra, the JobService instance will end its lifecycle (call 95 // onDestroy()) once jobFinished() is invoked. 96 Futures.addCallback( 97 cobaltLoggingFuture, 98 new FutureCallback<Void>() { 99 @Override 100 public void onSuccess(Void result) { 101 LogUtil.d("Cobalt logging job succeeded."); 102 103 // Tell the JobScheduler that the job has completed and does not 104 // need to be rescheduled. 105 boolean shouldRetry = false; 106 AdServicesJobServiceLogger.getInstance() 107 .recordJobFinished( 108 COBALT_LOGGING_JOB_ID, 109 /* isSuccessful= */ true, 110 shouldRetry); 111 jobFinished(params, shouldRetry); 112 } 113 114 @Override 115 public void onFailure(Throwable t) { 116 LogUtil.e(t, "Failed to handle cobalt logging job"); 117 118 // When failure, also tell the JobScheduler that the job has completed and 119 // does not need to be rescheduled. 120 boolean shouldRetry = false; 121 AdServicesJobServiceLogger.getInstance() 122 .recordJobFinished( 123 COBALT_LOGGING_JOB_ID, 124 /* isSuccessful= */ false, 125 shouldRetry); 126 jobFinished(params, shouldRetry); 127 } 128 }, 129 directExecutor()); 130 return true; 131 } 132 133 @Override onStopJob(JobParameters params)134 public boolean onStopJob(JobParameters params) { 135 LogUtil.d("CobaltJobService.onStopJob"); 136 // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the 137 // execution is completed or not to avoid executing the task twice. 138 boolean shouldRetry = false; 139 140 AdServicesJobServiceLogger.getInstance() 141 .recordOnStopJob(params, COBALT_LOGGING_JOB_ID, shouldRetry); 142 return shouldRetry; 143 } 144 145 // TODO(b/311183933): Remove passed in Context from static method. 146 @SuppressWarnings("AvoidStaticContext") 147 @VisibleForTesting schedule(Context context, JobScheduler jobScheduler, Flags flags)148 static void schedule(Context context, JobScheduler jobScheduler, Flags flags) { 149 JobInfo job = 150 new JobInfo.Builder( 151 COBALT_LOGGING_JOB_ID, 152 new ComponentName(context, CobaltJobService.class)) 153 .setRequiresCharging(true) 154 .setPersisted(true) 155 .setPeriodic(flags.getCobaltLoggingJobPeriodMs()) 156 .build(); 157 158 jobScheduler.schedule(job); 159 LogUtil.d("Scheduling cobalt logging job ..."); 160 } 161 162 /** 163 * Schedules cobalt Job Service if needed: there is no scheduled job with name job parameters. 164 * 165 * @param context the context 166 * @param forceSchedule a flag to indicate whether to force rescheduling the job. 167 * @return a {@code boolean} to indicate if the service job is actually scheduled. 168 */ 169 // TODO(b/311183933): Remove passed in Context from static method. 170 @SuppressWarnings("AvoidStaticContext") scheduleIfNeeded(Context context, boolean forceSchedule)171 public static boolean scheduleIfNeeded(Context context, boolean forceSchedule) { 172 Flags flags = FlagsFactory.getFlags(); 173 174 if (!flags.getCobaltLoggingEnabled()) { 175 LogUtil.e("Cobalt logging feature is disabled, skip scheduling the CobaltJobService."); 176 return false; 177 } 178 179 JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 180 if (jobScheduler == null) { 181 LogUtil.e("Cannot fetch job scheduler."); 182 return false; 183 } 184 185 long flagsCobaltJobPeriodMs = flags.getCobaltLoggingJobPeriodMs(); 186 JobInfo job = jobScheduler.getPendingJob(COBALT_LOGGING_JOB_ID); 187 if (job != null && !forceSchedule) { 188 long cobaltJobPeriodMs = job.getIntervalMillis(); 189 if (flagsCobaltJobPeriodMs == cobaltJobPeriodMs) { 190 LogUtil.i( 191 "Cobalt Job Service has been scheduled with same parameters, skip " 192 + "rescheduling."); 193 return false; 194 } 195 } 196 197 schedule(context, jobScheduler, flags); 198 return true; 199 } 200 skipAndCancelBackgroundJob( JobParameters params, int jobId, int skipReason, boolean doRecord)201 private boolean skipAndCancelBackgroundJob( 202 JobParameters params, int jobId, int skipReason, boolean doRecord) { 203 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 204 if (jobScheduler != null) { 205 jobScheduler.cancel(COBALT_LOGGING_JOB_ID); 206 } 207 208 if (doRecord) { 209 AdServicesJobServiceLogger.getInstance().recordJobSkipped(jobId, skipReason); 210 } 211 212 // Tell the JobScheduler that the job has completed and does not need to be 213 // rescheduled. 214 jobFinished(params, false); 215 216 // Returning false means that this job has completed its work. 217 return false; 218 } 219 } 220