1 /* 2 * Copyright (C) 2024 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 package com.android.adservices.service.common; 17 18 import static com.android.adservices.spe.AdServicesJobInfo.AD_PACKAGE_DENY_PRE_PROCESS_JOB; 19 20 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 21 22 import android.app.job.JobInfo; 23 import android.app.job.JobParameters; 24 import android.app.job.JobScheduler; 25 import android.app.job.JobService; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.os.Build; 29 30 import androidx.annotation.RequiresApi; 31 import androidx.annotation.VisibleForTesting; 32 33 import com.android.adservices.LogUtil; 34 import com.android.adservices.concurrency.AdServicesExecutors; 35 import com.android.adservices.service.FlagsFactory; 36 import com.android.adservices.service.common.compat.ServiceCompatUtils; 37 import com.android.adservices.shared.common.ApplicationContextSingleton; 38 import com.android.adservices.spe.AdServicesJobServiceLogger; 39 40 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 41 import com.google.common.util.concurrent.FutureCallback; 42 import com.google.common.util.concurrent.Futures; 43 import com.google.common.util.concurrent.ListenableFuture; 44 45 /** 46 * Class schedules a periodic job to load package deny data from mdd into cache on Android S where 47 * adding a new package does not trigger an event for adservices. 48 */ 49 @RequiresApi(Build.VERSION_CODES.S) 50 public final class AdPackageDenyPreProcessJobService extends JobService { 51 52 @Override onStartJob(JobParameters params)53 public boolean onStartJob(JobParameters params) { 54 // Always ensure that the first thing this job does is check if it should be running, and 55 // cancel itself if it's not supposed to be. 56 if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { 57 LogUtil.d( 58 "Disabling AdPackageDenyPreProcessJobService because it's running in" 59 + " ExtServices on T+"); 60 // Do not log via the AdservicesJobServiceLogger because the it might cause 61 // ClassNotFound exception on earlier beta versions. 62 return skipAndCancelBackgroundJob(params); 63 } 64 65 // Record the invocation of onStartJob() for logging purpose. 66 LogUtil.d("AdPackageDenyPreProcessJobService.onStartJob"); 67 AdServicesJobServiceLogger.getInstance() 68 .recordOnStartJob(AD_PACKAGE_DENY_PRE_PROCESS_JOB.getJobId()); 69 if (!FlagsFactory.getFlags().getEnablePackageDenyBgJob()) { 70 return skipAndCancelBackgroundJob(params); 71 } 72 ListenableFuture<AdPackageDenyResolver.PackageDenyMddProcessStatus> future = 73 PropagatedFutures.submitAsync( 74 () -> { 75 LogUtil.d("AdPackageDenyPreProcessJobService.onStart Job."); 76 return AdPackageDenyResolver.getInstance().loadDenyDataFromMdd(); 77 }, 78 AdServicesExecutors.getBlockingExecutor()); 79 80 // Background job logging in onSuccess and OnFailure have to happen before jobFinished() is 81 // called. Due to JobScheduler infra, the JobService instance will end its lifecycle (call 82 // onDestroy()) once jobFinished() is invoked. 83 Futures.addCallback( 84 future, 85 new FutureCallback<AdPackageDenyResolver.PackageDenyMddProcessStatus>() { 86 @Override 87 public void onSuccess( 88 AdPackageDenyResolver.PackageDenyMddProcessStatus result) { 89 LogUtil.d("AdPackageDenyPreProcessJobService job succeeded."); 90 91 // Tell the JobScheduler that the job has completed and does not 92 // need to be rescheduled. 93 boolean shouldRetry = false; 94 AdServicesJobServiceLogger.getInstance() 95 .recordJobFinished( 96 AD_PACKAGE_DENY_PRE_PROCESS_JOB.getJobId(), 97 /* isSuccessful= */ true, 98 shouldRetry); 99 jobFinished(params, shouldRetry); 100 } 101 102 @Override 103 public void onFailure(Throwable t) { 104 LogUtil.e(t, "Failed to handle AdPackageDenyPreProcessJobService job"); 105 106 // When failure, also tell the JobScheduler that the job has completed and 107 // does not need to be rescheduled. 108 boolean shouldRetry = false; 109 AdServicesJobServiceLogger.getInstance() 110 .recordJobFinished( 111 AD_PACKAGE_DENY_PRE_PROCESS_JOB.getJobId(), 112 /* isSuccessful= */ false, 113 shouldRetry); 114 jobFinished(params, shouldRetry); 115 } 116 }, 117 directExecutor()); 118 return true; 119 } 120 121 @Override onStopJob(JobParameters params)122 public boolean onStopJob(JobParameters params) { 123 LogUtil.d("AdPackageDenyPreProcessJobService.onStopJob"); 124 // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the 125 // execution is completed or not to avoid executing the task twice. 126 boolean shouldRetry = false; 127 128 AdServicesJobServiceLogger.getInstance() 129 .recordOnStopJob(params, AD_PACKAGE_DENY_PRE_PROCESS_JOB.getJobId(), shouldRetry); 130 return shouldRetry; 131 } 132 133 /** 134 * Schedules AdPackageDenyPreProcessJobService if needed: there is no scheduled job with name 135 * job parameters. 136 * 137 * @return a {@code boolean} to indicate if the service job is actually scheduled. 138 */ scheduleIfNeeded()139 public static boolean scheduleIfNeeded() { 140 if (!FlagsFactory.getFlags().getEnablePackageDenyBgJob()) { 141 LogUtil.d("AdPackageDenyPreProcessJobService is disabled"); 142 return false; 143 } 144 Context context = ApplicationContextSingleton.get(); 145 JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 146 if (jobScheduler == null) { 147 LogUtil.e("Cannot fetch job scheduler."); 148 return false; 149 } 150 JobInfo scheduledJobJobInfo = 151 jobScheduler.getPendingJob(AD_PACKAGE_DENY_PRE_PROCESS_JOB.getJobId()); 152 final JobInfo job = buildJobInfo(context); 153 if (job.equals(scheduledJobJobInfo)) { 154 LogUtil.i( 155 "AdPackageDenyPreProcessJobService has been scheduled with same " 156 + "parameters, skip " 157 + "rescheduling."); 158 } else { 159 schedule(job, jobScheduler); 160 } 161 return true; 162 } 163 164 @VisibleForTesting schedule(JobInfo job, JobScheduler jobScheduler)165 static void schedule(JobInfo job, JobScheduler jobScheduler) { 166 jobScheduler.schedule(job); 167 LogUtil.d("Scheduling AdDenyAppPreProcessorJobService job ..."); 168 } 169 170 // TODO(b/311183933): Remove passed in Context from static method. 171 @SuppressWarnings("AvoidStaticContext") buildJobInfo(Context context)172 private static JobInfo buildJobInfo(Context context) { 173 // TODO(b/374326249) move periodic flex time config to flags 174 long flexIntervalMillis = 60 * 60 * 1000; // 1 hr 175 return new JobInfo.Builder( 176 AD_PACKAGE_DENY_PRE_PROCESS_JOB.getJobId(), 177 new ComponentName(context, AdPackageDenyPreProcessJobService.class)) 178 .setPeriodic( 179 FlagsFactory.getFlags().getPackageDenyBackgroundJobPeriodMillis(), 180 flexIntervalMillis) 181 .setPersisted(true) 182 .build(); 183 } 184 skipAndCancelBackgroundJob(JobParameters params)185 private boolean skipAndCancelBackgroundJob(JobParameters params) { 186 LogUtil.d("Cancelling the AdPackageDenyPreProcessJobService job"); 187 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 188 if (jobScheduler != null) { 189 jobScheduler.cancel(AD_PACKAGE_DENY_PRE_PROCESS_JOB.getJobId()); 190 } 191 AdServicesJobServiceLogger.getInstance() 192 .recordJobSkipped(AD_PACKAGE_DENY_PRE_PROCESS_JOB.getJobId(), 0); 193 jobFinished(params, false); 194 return false; 195 } 196 } 197