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.adselection;
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.FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB;
23 
24 import android.annotation.SuppressLint;
25 import android.app.job.JobInfo;
26 import android.app.job.JobParameters;
27 import android.app.job.JobScheduler;
28 import android.app.job.JobService;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.os.Build;
32 
33 import androidx.annotation.RequiresApi;
34 
35 import com.android.adservices.LogUtil;
36 import com.android.adservices.LoggerFactory;
37 import com.android.adservices.concurrency.AdServicesExecutors;
38 import com.android.adservices.service.Flags;
39 import com.android.adservices.service.FlagsFactory;
40 import com.android.adservices.service.common.compat.ServiceCompatUtils;
41 import com.android.adservices.service.consent.AdServicesApiType;
42 import com.android.adservices.service.consent.ConsentManager;
43 import com.android.adservices.spe.AdServicesJobServiceLogger;
44 import com.android.internal.annotations.VisibleForTesting;
45 
46 import com.google.common.util.concurrent.FutureCallback;
47 
48 import java.time.Clock;
49 import java.time.Instant;
50 import java.util.concurrent.ExecutionException;
51 import java.util.concurrent.TimeoutException;
52 
53 /**
54  * Debug report sender for FLEDGE Select Ads API, executing periodic pinging of debug reports and
55  * cleanup..
56  */
57 @SuppressLint("LineLength")
58 @RequiresApi(Build.VERSION_CODES.S)
59 public class DebugReportSenderJobService extends JobService {
60     private static final int FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID =
61             FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB.getJobId();
62 
63     @Override
onStartJob(JobParameters params)64     public boolean onStartJob(JobParameters params) {
65         // Always ensure that the first thing this job does is check if it should be running, and
66         // cancel itself if it's not supposed to be.
67         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
68             LogUtil.d(
69                     "Disabling DebugReportSenderJobService job because it's running in "
70                             + " ExtServices on T+");
71             return skipAndCancelKeyFetchJob(
72                     params,
73                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS,
74                     false);
75         }
76         LoggerFactory.getFledgeLogger().d("DebugReportSenderJobService.onStartJob");
77 
78         AdServicesJobServiceLogger.getInstance()
79                 .recordOnStartJob(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID);
80 
81         if (FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) {
82             LoggerFactory.getFledgeLogger()
83                     .d("FLEDGE Ad Selection API is disabled ; skipping and cancelling job");
84             return skipAndCancelKeyFetchJob(
85                     params,
86                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
87                     true);
88         }
89 
90         if (!FlagsFactory.getFlags().getFledgeEventLevelDebugReportingEnabled()) {
91             LoggerFactory.getFledgeLogger()
92                     .d(
93                             "FLEDGE Ad Selection Debug Reporting  is disabled ; skipping and"
94                                     + " cancelling job");
95             return skipAndCancelKeyFetchJob(
96                     params,
97                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON,
98                     true);
99         }
100 
101         // Skip the execution and cancel the job if user consent is revoked.
102         // Use the per-API consent with GA UX.
103         if (!ConsentManager.getInstance().getConsent(AdServicesApiType.FLEDGE).isGiven()) {
104             LoggerFactory.getFledgeLogger()
105                     .d("User Consent is revoked ; skipping and cancelling job");
106             return skipAndCancelKeyFetchJob(
107                     params,
108                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED,
109                     true);
110         }
111 
112         // TODO(b/235841960): Consider using com.android.adservices.service.stats.Clock instead of
113         //  Java Clock
114         Instant jobStartTime = Clock.systemUTC().instant();
115         LoggerFactory.getFledgeLogger()
116                 .d(
117                         "Starting FLEDGE DebugReportSenderJobService job at %s",
118                         jobStartTime.toString());
119 
120         DebugReportSenderWorker.getInstance(this)
121                 .runDebugReportSender()
122                 .addCallback(
123                         new FutureCallback<Void>() {
124                             // Never manually reschedule the background fetch job, since it is
125                             // already scheduled periodically and should try again multiple times
126                             // per day
127                             @Override
128                             public void onSuccess(Void result) {
129                                 boolean shouldRetry = false;
130                                 AdServicesJobServiceLogger.getInstance()
131                                         .recordJobFinished(
132                                                 FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID,
133                                                 /* isSuccessful= */ true,
134                                                 shouldRetry);
135 
136                                 jobFinished(params, shouldRetry);
137                             }
138 
139                             @Override
140                             public void onFailure(Throwable t) {
141                                 logExceptionMessage(t);
142                                 boolean shouldRetry = false;
143                                 AdServicesJobServiceLogger.getInstance()
144                                         .recordJobFinished(
145                                                 FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID,
146                                                 /* isSuccessful= */ false,
147                                                 shouldRetry);
148 
149                                 jobFinished(params, shouldRetry);
150                             }
151                         },
152                         AdServicesExecutors.getLightWeightExecutor());
153         return true;
154     }
155 
logExceptionMessage(Throwable t)156     private void logExceptionMessage(Throwable t) {
157         if (t instanceof InterruptedException) {
158             LoggerFactory.getFledgeLogger()
159                     .e(t, "FLEDGE DebugReport Sender JobService interrupted");
160         } else if (t instanceof ExecutionException) {
161             LoggerFactory.getFledgeLogger()
162                     .e(t, "FLEDGE DebugReport Sender JobService failed due to internal error");
163         } else if (t instanceof TimeoutException) {
164             LoggerFactory.getFledgeLogger()
165                     .e(t, "FLEDGE DebugReport Sender JobService timeout exceeded");
166         } else {
167             LoggerFactory.getFledgeLogger()
168                     .e(t, "FLEDGE DebugReport Sender JobService failed due to unexpected error");
169         }
170     }
171 
skipAndCancelKeyFetchJob( final JobParameters params, int skipReason, boolean doRecord)172     private boolean skipAndCancelKeyFetchJob(
173             final JobParameters params, int skipReason, boolean doRecord) {
174         this.getSystemService(JobScheduler.class)
175                 .cancel(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID);
176         if (doRecord) {
177             AdServicesJobServiceLogger.getInstance()
178                     .recordJobSkipped(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID, skipReason);
179         }
180         jobFinished(params, false);
181         return false;
182     }
183 
184     @Override
onStopJob(JobParameters params)185     public boolean onStopJob(JobParameters params) {
186         LoggerFactory.getFledgeLogger().d("DebugReportSenderJobService.onStopJob");
187         DebugReportSenderWorker.getInstance(this).stopWork();
188 
189         boolean shouldRetry = true;
190 
191         AdServicesJobServiceLogger.getInstance()
192                 .recordOnStopJob(
193                         params, FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID, shouldRetry);
194         return shouldRetry;
195     }
196 
197     /**
198      * Attempts to schedule the FLEDGE Ad Selection debug report sender as a singleton periodic job
199      * if it is not already scheduled.
200      *
201      * <p>The debug report sender job primarily sends debug reports generated for ad selections. It
202      * also prunes the ad selection debug report database of any expired data.
203      */
scheduleIfNeeded(Context context, boolean forceSchedule)204     public static void scheduleIfNeeded(Context context, boolean forceSchedule) {
205         Flags flags = FlagsFactory.getFlags();
206         if (!flags.getFledgeEventLevelDebugReportingEnabled()) {
207             LoggerFactory.getFledgeLogger()
208                     .v("FLEDGE Ad selection Debug reporting is disabled; skipping schedule");
209             return;
210         }
211 
212         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
213 
214         // Scheduling a job can be expensive, and forcing a schedule could interrupt a job that is
215         // already in progress
216         // TODO(b/221837833): Intelligently decide when to overwrite a scheduled job
217         if ((jobScheduler.getPendingJob(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID) == null)
218                 || forceSchedule) {
219             schedule(context, flags);
220             LoggerFactory.getFledgeLogger()
221                     .d("Scheduled FLEDGE ad selection Debug report sender job");
222         } else {
223             LoggerFactory.getFledgeLogger()
224                     .v(
225                             "FLEDGE ad selection Debug report sender job already scheduled,"
226                                     + " skipping reschedule");
227         }
228     }
229 
230     /**
231      * Actually schedules the FLEDGE Ad Selection Debug Report sender job as a singleton periodic
232      * job.
233      *
234      * <p>Split out from {@link #scheduleIfNeeded(Context, boolean)} for mockable testing without
235      * pesky permissions.
236      */
237     @VisibleForTesting
schedule(Context context, Flags flags)238     protected static void schedule(Context context, Flags flags) {
239         if (!flags.getFledgeEventLevelDebugReportingEnabled()) {
240             LoggerFactory.getFledgeLogger()
241                     .v("FLEDGE Ad selection Debug reporting is disabled; skipping schedule");
242             return;
243         }
244 
245         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
246         final JobInfo job =
247                 new JobInfo.Builder(
248                                 FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID,
249                                 new ComponentName(context, DebugReportSenderJobService.class))
250                         .setRequiresBatteryNotLow(true)
251                         .setRequiresDeviceIdle(true)
252                         .setPeriodic(
253                                 flags.getFledgeDebugReportSenderJobPeriodMs(),
254                                 flags.getFledgeDebugReportSenderJobFlexMs())
255                         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
256                         .setPersisted(true)
257                         .build();
258         jobScheduler.schedule(job);
259     }
260 }
261