• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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