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.service.signals;
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_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED;
22 import static com.android.adservices.spe.AdServicesJobInfo.PERIODIC_SIGNALS_ENCODING_JOB;
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 
34 import com.android.adservices.LogUtil;
35 import com.android.adservices.LoggerFactory;
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.service.consent.AdServicesApiType;
41 import com.android.adservices.service.consent.ConsentManager;
42 import com.android.adservices.spe.AdServicesJobServiceLogger;
43 import com.android.internal.annotations.VisibleForTesting;
44 
45 import com.google.common.util.concurrent.FutureCallback;
46 
47 /**
48  * Periodic encoding background job, periodically encodes raw protected signals into encoded
49  * payloads. Also cleans up stale encoding logic and encoded results.
50  */
51 @RequiresApi(Build.VERSION_CODES.S)
52 public class PeriodicEncodingJobService extends JobService {
53 
54     private static final int PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID =
55             PERIODIC_SIGNALS_ENCODING_JOB.getJobId();
56 
57     @Override
onStartJob(JobParameters params)58     public boolean onStartJob(JobParameters params) {
59 
60         // If job is not supposed to be running, cancel itself.
61         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
62             LogUtil.d(
63                     "Disabling PeriodicEncodingJobService job because it's running in ExtServices"
64                             + " on T+");
65             return skipAndCancelBackgroundJob(
66                     params,
67                     /* skipReason=*/ AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS,
68                     /* doRecord=*/ false);
69         }
70 
71         LoggerFactory.getFledgeLogger().d("PeriodicEncodingJobService.onStartJob");
72 
73         AdServicesJobServiceLogger.getInstance()
74                 .recordOnStartJob(PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID);
75 
76         if (!FlagsFactory.getFlags().getProtectedSignalsPeriodicEncodingEnabled()) {
77             LoggerFactory.getFledgeLogger()
78                     .d("FLEDGE periodic encoding is disabled; skipping and cancelling job");
79             return skipAndCancelBackgroundJob(
80                     params,
81                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
82                     /* doRecord=*/ true);
83         }
84 
85         if (!FlagsFactory.getFlags().getProtectedSignalsEnabled()) {
86             LoggerFactory.getFledgeLogger()
87                     .d("FLEDGE Protected Signals API is disabled ; skipping and cancelling job");
88             return skipAndCancelBackgroundJob(
89                     params,
90                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
91                     /* doRecord=*/ true);
92         }
93 
94         // Skip the execution and cancel the job if user consent is revoked.
95         // Use the per-API consent with GA UX.
96         if (!ConsentManager.getInstance().getConsent(AdServicesApiType.FLEDGE).isGiven()) {
97             LoggerFactory.getFledgeLogger()
98                     .d("User Consent is revoked ; skipping and cancelling job");
99             return skipAndCancelBackgroundJob(
100                     params,
101                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED,
102                     /* doRecord=*/ true);
103         }
104 
105         PeriodicEncodingJobWorker encodingWorker = PeriodicEncodingJobWorker.getInstance();
106         encodingWorker
107                 .encodeProtectedSignals()
108                 .addCallback(
109                         new FutureCallback<Void>() {
110                             @Override
111                             public void onSuccess(Void result) {
112                                 LoggerFactory.getFledgeLogger()
113                                         .d("PeriodicEncodingJobService encoding completed");
114 
115                                 boolean shouldRetry = false;
116                                 AdServicesJobServiceLogger.getInstance()
117                                         .recordJobFinished(
118                                                 PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID,
119                                                 /* isSuccessful= */ true,
120                                                 shouldRetry);
121 
122                                 jobFinished(params, shouldRetry);
123                             }
124 
125                             @Override
126                             public void onFailure(Throwable t) {
127                                 boolean shouldRetry = false;
128                                 AdServicesJobServiceLogger.getInstance()
129                                         .recordJobFinished(
130                                                 PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID,
131                                                 /* isSuccessful= */ false,
132                                                 shouldRetry);
133 
134                                 jobFinished(params, shouldRetry);
135                             }
136                         },
137                         AdServicesExecutors.getLightWeightExecutor());
138         return true;
139     }
140 
141     @Override
onStopJob(JobParameters params)142     public boolean onStopJob(JobParameters params) {
143         LoggerFactory.getFledgeLogger().d("PeriodicEncodingJobService.onStopJob");
144         PeriodicEncodingJobWorker.getInstance().stopWork();
145 
146         boolean shouldRetry = true;
147         AdServicesJobServiceLogger.getInstance()
148                 .recordOnStopJob(params, PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID, shouldRetry);
149 
150         return shouldRetry;
151     }
152 
153     /**
154      * Attempts to schedule the Periodic encoding as a singleton job if it is not already scheduled.
155      */
scheduleIfNeeded(Context context, Flags flags, boolean forceSchedule)156     public static void scheduleIfNeeded(Context context, Flags flags, boolean forceSchedule) {
157         LoggerFactory.getFledgeLogger()
158                 .v(
159                         "Attempting to schedule job:%s if needed",
160                         PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID);
161         if (!flags.getProtectedSignalsPeriodicEncodingEnabled()) {
162             LoggerFactory.getFledgeLogger()
163                     .v("Protected Signals periodic encoding is disabled; skipping schedule");
164             return;
165         }
166 
167         if (!flags.getProtectedSignalsEnabled()) {
168             LoggerFactory.getFledgeLogger()
169                     .d("FLEDGE Protected Signals API is disabled ; skipping and cancelling job");
170             return;
171         }
172 
173         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
174         JobInfo job = jobScheduler.getPendingJob(PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID);
175         long existingJobPeriodMillis = flags.getProtectedSignalPeriodicEncodingJobPeriodMs();
176         if (job == null
177                 || forceSchedule
178                 // Reschedule the job if the period flag has changed
179                 || (job.getIntervalMillis() != existingJobPeriodMillis
180                         && JobInfo.getMinPeriodMillis() < existingJobPeriodMillis)) {
181             schedule(context, flags);
182         } else {
183             LoggerFactory.getFledgeLogger()
184                     .v(
185                             "Protected Signals periodic encoding job already scheduled, skipping "
186                                     + "reschedule");
187         }
188     }
189 
190     /**
191      * Actually schedules the Periodic Encoding as a singleton periodic job.
192      *
193      * <p>Split out from {@link #scheduleIfNeeded(Context, Flags, boolean)} for mockable testing
194      */
195     @VisibleForTesting
schedule(Context context, Flags flags)196     protected static void schedule(Context context, Flags flags) {
197         if (!flags.getProtectedSignalsPeriodicEncodingEnabled()) {
198             LoggerFactory.getFledgeLogger()
199                     .v("Protected Signals periodic encoding is disabled; skipping schedule");
200             return;
201         }
202 
203         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
204         final JobInfo job =
205                 new JobInfo.Builder(
206                                 PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID,
207                                 new ComponentName(context, PeriodicEncodingJobService.class))
208                         .setRequiresBatteryNotLow(true)
209                         .setPeriodic(
210                                 flags.getProtectedSignalPeriodicEncodingJobPeriodMs(),
211                                 flags.getProtectedSignalsPeriodicEncodingJobFlexMs())
212                         .setPersisted(true)
213                         .build();
214         jobScheduler.schedule(job);
215     }
216 
skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)217     private boolean skipAndCancelBackgroundJob(
218             final JobParameters params, int skipReason, boolean doRecord) {
219         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
220         if (jobScheduler != null) {
221             jobScheduler.cancel(PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID);
222         }
223 
224         if (doRecord) {
225             AdServicesJobServiceLogger.getInstance()
226                     .recordJobSkipped(PROTECTED_SIGNALS_PERIODIC_ENCODING_JOB_ID, skipReason);
227         }
228 
229         jobFinished(params, false);
230         return false;
231     }
232 }
233