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