• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.topics;
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_ERROR_REPORTED__ERROR_CODE__TOPICS_API_DISABLED;
22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_FETCH_JOB_SCHEDULER_FAILURE;
23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_HANDLE_JOB_SERVICE_FAILURE;
24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS;
25 import static com.android.adservices.spe.AdservicesJobInfo.TOPICS_EPOCH_JOB;
26 
27 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
28 
29 import android.annotation.NonNull;
30 import android.app.job.JobInfo;
31 import android.app.job.JobParameters;
32 import android.app.job.JobScheduler;
33 import android.app.job.JobService;
34 import android.content.ComponentName;
35 import android.content.Context;
36 import android.os.Build;
37 
38 import androidx.annotation.RequiresApi;
39 
40 import com.android.adservices.LogUtil;
41 import com.android.adservices.LoggerFactory;
42 import com.android.adservices.concurrency.AdServicesExecutors;
43 import com.android.adservices.errorlogging.ErrorLogUtil;
44 import com.android.adservices.service.FlagsFactory;
45 import com.android.adservices.service.common.compat.ServiceCompatUtils;
46 import com.android.adservices.spe.AdservicesJobServiceLogger;
47 
48 import com.google.common.annotations.VisibleForTesting;
49 import com.google.common.util.concurrent.FutureCallback;
50 import com.google.common.util.concurrent.Futures;
51 import com.google.common.util.concurrent.ListenableFuture;
52 
53 /** Epoch computation job. This will be run approximately once per epoch to compute Topics. */
54 // TODO(b/269798827): Enable for R.
55 @RequiresApi(Build.VERSION_CODES.S)
56 public final class EpochJobService extends JobService {
57     private static final int TOPICS_EPOCH_JOB_ID = TOPICS_EPOCH_JOB.getJobId();
58 
59     @Override
onStartJob(JobParameters params)60     public boolean onStartJob(JobParameters params) {
61         // Always ensure that the first thing this job does is check if it should be running, and
62         // cancel itself if it's not supposed to be.
63         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
64             LogUtil.d("Disabling EpochJobService job because it's running in ExtServices on T+");
65             return skipAndCancelBackgroundJob(
66                     params,
67                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS);
68         }
69 
70         LoggerFactory.getTopicsLogger().d("EpochJobService.onStartJob");
71 
72         AdservicesJobServiceLogger.getInstance(this).recordOnStartJob(TOPICS_EPOCH_JOB_ID);
73 
74         if (FlagsFactory.getFlags().getTopicsKillSwitch()) {
75             ErrorLogUtil.e(
76                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_API_DISABLED,
77                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS,
78                     this.getClass().getSimpleName(),
79                     new Object() {}.getClass().getEnclosingMethod().getName());
80             LoggerFactory.getTopicsLogger()
81                     .e("Topics API is disabled, skipping and cancelling EpochJobService");
82             return skipAndCancelBackgroundJob(
83                     params,
84                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
85         }
86 
87         // This service executes each incoming job on a Handler running on the application's
88         // main thread. This means that we must offload the execution logic to background executor.
89         // TODO(b/225382268): Handle cancellation.
90         ListenableFuture<Void> epochComputationFuture =
91                 Futures.submit(
92                         () -> {
93                             TopicsWorker.getInstance(this).computeEpoch();
94                         },
95                         AdServicesExecutors.getBackgroundExecutor());
96 
97         Futures.addCallback(
98                 epochComputationFuture,
99                 new FutureCallback<Void>() {
100                     @Override
101                     public void onSuccess(Void result) {
102                         LoggerFactory.getTopicsLogger().d("Epoch Computation succeeded!");
103 
104                         boolean shouldRetry = false;
105                         AdservicesJobServiceLogger.getInstance(EpochJobService.this)
106                                 .recordJobFinished(
107                                         TOPICS_EPOCH_JOB_ID, /* isSuccessful= */ true, shouldRetry);
108 
109                         // Tell the JobScheduler that the job has completed and does not need to be
110                         // rescheduled.
111                         jobFinished(params, shouldRetry);
112                     }
113 
114                     @Override
115                     public void onFailure(Throwable t) {
116                         ErrorLogUtil.e(
117                                 t,
118                                 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_HANDLE_JOB_SERVICE_FAILURE,
119                                 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
120                         LoggerFactory.getTopicsLogger()
121                                 .e(t, "Failed to handle JobService: " + params.getJobId());
122 
123                         boolean shouldRetry = false;
124                         AdservicesJobServiceLogger.getInstance(EpochJobService.this)
125                                 .recordJobFinished(
126                                         TOPICS_EPOCH_JOB_ID,
127                                         /* isSuccessful= */ false,
128                                         shouldRetry);
129 
130                         //  When failure, also tell the JobScheduler that the job has completed and
131                         // does not need to be rescheduled.
132                         // TODO(b/225909845): Revisit this. We need a retry policy.
133                         jobFinished(params, shouldRetry);
134                     }
135                 },
136                 directExecutor());
137 
138         return true;
139     }
140 
141     @Override
onStopJob(JobParameters params)142     public boolean onStopJob(JobParameters params) {
143         LoggerFactory.getTopicsLogger().d("EpochJobService.onStopJob");
144 
145         // Tell JobScheduler not to reschedule the job because it's unknown at this stage if the
146         // execution is completed or not to avoid executing the task twice.
147         boolean shouldRetry = false;
148 
149         AdservicesJobServiceLogger.getInstance(this)
150                 .recordOnStopJob(params, TOPICS_EPOCH_JOB_ID, shouldRetry);
151         return shouldRetry;
152     }
153 
154     @VisibleForTesting
schedule( Context context, @NonNull JobScheduler jobScheduler, long epochJobPeriodMs, long epochJobFlexMs)155     static void schedule(
156             Context context,
157             @NonNull JobScheduler jobScheduler,
158             long epochJobPeriodMs,
159             long epochJobFlexMs) {
160         final JobInfo job =
161                 new JobInfo.Builder(
162                                 TOPICS_EPOCH_JOB_ID,
163                                 new ComponentName(context, EpochJobService.class))
164                         .setRequiresCharging(true)
165                         .setPersisted(true)
166                         .setPeriodic(epochJobPeriodMs, epochJobFlexMs)
167                         .build();
168 
169         jobScheduler.schedule(job);
170         LoggerFactory.getTopicsLogger().d("Scheduling Epoch job ...");
171     }
172 
173     /**
174      * Schedule Epoch Job Service if needed: there is no scheduled job with same job parameters.
175      *
176      * @param context the context
177      * @param forceSchedule a flag to indicate whether to force rescheduling the job.
178      * @return a {@code boolean} to indicate if the service job is actually scheduled.
179      */
scheduleIfNeeded(Context context, boolean forceSchedule)180     public static boolean scheduleIfNeeded(Context context, boolean forceSchedule) {
181         if (FlagsFactory.getFlags().getTopicsKillSwitch()) {
182             ErrorLogUtil.e(
183                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_API_DISABLED,
184                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS,
185                     new Object() {}.getClass().getSimpleName(),
186                     new Object() {}.getClass().getEnclosingMethod().getName());
187             LoggerFactory.getTopicsLogger()
188                     .e("Topics API is disabled, skip scheduling the EpochJobService");
189             return false;
190         }
191 
192         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
193         if (jobScheduler == null) {
194             ErrorLogUtil.e(
195                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_FETCH_JOB_SCHEDULER_FAILURE,
196                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS,
197                     new Object() {}.getClass().getSimpleName(),
198                     new Object() {}.getClass().getEnclosingMethod().getName());
199             LoggerFactory.getTopicsLogger().e("Cannot fetch Job Scheduler!");
200             return false;
201         }
202 
203         long flagsEpochJobPeriodMs = FlagsFactory.getFlags().getTopicsEpochJobPeriodMs();
204         long flagsEpochJobFlexMs = FlagsFactory.getFlags().getTopicsEpochJobFlexMs();
205 
206         JobInfo job = jobScheduler.getPendingJob(TOPICS_EPOCH_JOB_ID);
207         // Skip to reschedule the job if there is same scheduled job with same parameters.
208         if (job != null && !forceSchedule) {
209             long epochJobPeriodMs = job.getIntervalMillis();
210             long epochJobFlexMs = job.getFlexMillis();
211 
212             if (flagsEpochJobPeriodMs == epochJobPeriodMs
213                     && flagsEpochJobFlexMs == epochJobFlexMs) {
214                 LoggerFactory.getTopicsLogger()
215                         .i(
216                                 "Epoch Job Service has been scheduled with same parameters, skip"
217                                         + " rescheduling!");
218                 return false;
219             }
220         }
221 
222         schedule(context, jobScheduler, flagsEpochJobPeriodMs, flagsEpochJobFlexMs);
223         return true;
224     }
225 
skipAndCancelBackgroundJob(final JobParameters params, int skipReason)226     private boolean skipAndCancelBackgroundJob(final JobParameters params, int skipReason) {
227         this.getSystemService(JobScheduler.class).cancel(TOPICS_EPOCH_JOB_ID);
228 
229         AdservicesJobServiceLogger.getInstance(this)
230                 .recordJobSkipped(TOPICS_EPOCH_JOB_ID, skipReason);
231 
232         // Tell the JobScheduler that the job has completed and does not need to be
233         // rescheduled.
234         jobFinished(params, false);
235 
236         // Returning false means that this job has completed its work.
237         return false;
238     }
239 }
240