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