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.common;
18 
19 import static com.android.adservices.data.common.AdservicesEntryPointConstant.FIRST_ENTRY_REQUEST_TIMESTAMP;
20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__LOAD_MDD_FILE_GROUP_FAILURE;
21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX;
22 import static com.android.adservices.service.ui.ux.collection.PrivacySandboxUxCollection.RVC_UX;
23 import static com.android.adservices.spe.AdServicesJobInfo.CONSENT_NOTIFICATION_JOB;
24 
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.content.SharedPreferences;
32 import android.os.Build;
33 import android.os.PersistableBundle;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.RequiresApi;
37 
38 import com.android.adservices.LogUtil;
39 import com.android.adservices.concurrency.AdServicesExecutors;
40 import com.android.adservices.download.MddJob;
41 import com.android.adservices.download.MobileDataDownloadFactory;
42 import com.android.adservices.errorlogging.ErrorLogUtil;
43 import com.android.adservices.service.Flags;
44 import com.android.adservices.service.FlagsFactory;
45 import com.android.adservices.service.common.compat.ServiceCompatUtils;
46 import com.android.adservices.service.consent.ConsentManager;
47 import com.android.adservices.service.ui.data.UxStatesManager;
48 import com.android.adservices.spe.AdServicesJobServiceLogger;
49 
50 import com.google.android.libraries.mobiledatadownload.GetFileGroupRequest;
51 import com.google.mobiledatadownload.ClientConfigProto.ClientFileGroup;
52 
53 import java.util.Calendar;
54 import java.util.TimeZone;
55 import java.util.concurrent.ExecutionException;
56 
57 /**
58  * Consent Notification job. This will be run every day during acceptable hours (provided by PH
59  * flags) to trigger the Notification for Privacy Sandbox.
60  */
61 // TODO(b/269798827): Enable for R.
62 @RequiresApi(Build.VERSION_CODES.S)
63 public class ConsentNotificationJobService extends JobService {
64     static final int CONSENT_NOTIFICATION_JOB_ID = CONSENT_NOTIFICATION_JOB.getJobId();
65     static final long MILLISECONDS_IN_THE_DAY = 86400000L;
66 
67     static final String ADID_ENABLE_STATUS = "adid_enable_status";
68     static final String RE_CONSENT_STATUS = "re_consent_status";
69     private static final String ADSERVICES_STATUS_SHARED_PREFERENCE =
70             "AdserviceStatusSharedPreference";
71 
72     private ConsentManager mConsentManager;
73 
74     private UxStatesManager mUxStatesManager;
75 
76     /** Schedule the Job. */
schedule(Context context, boolean adidEnabled, boolean reConsentStatus)77     public static void schedule(Context context, boolean adidEnabled, boolean reConsentStatus) {
78         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
79         long initialDelay = calculateInitialDelay(Calendar.getInstance(TimeZone.getDefault()));
80         long deadline = calculateDeadline(Calendar.getInstance(TimeZone.getDefault()));
81         LogUtil.d("initial delay is " + initialDelay + ", deadline is " + deadline);
82 
83         SharedPreferences sharedPref =
84                 context.getSharedPreferences(
85                         ADSERVICES_STATUS_SHARED_PREFERENCE, Context.MODE_PRIVATE);
86 
87         long currentTimestamp = System.currentTimeMillis();
88         long firstEntryRequestTimestamp =
89                 sharedPref.getLong(FIRST_ENTRY_REQUEST_TIMESTAMP, currentTimestamp);
90         if (firstEntryRequestTimestamp == currentTimestamp) {
91             // schedule the background download tasks for OTA resources at the first PPAPI request.
92             MddJob.scheduleAllMddJobs();
93             SharedPreferences.Editor editor = sharedPref.edit();
94             editor.putLong(FIRST_ENTRY_REQUEST_TIMESTAMP, currentTimestamp);
95             if (!editor.commit()) {
96                 LogUtil.e("Failed to save " + FIRST_ENTRY_REQUEST_TIMESTAMP);
97             }
98         }
99         LogUtil.d(FIRST_ENTRY_REQUEST_TIMESTAMP + ": " + firstEntryRequestTimestamp);
100 
101         PersistableBundle bundle = new PersistableBundle();
102         bundle.putBoolean(ADID_ENABLE_STATUS, adidEnabled);
103         bundle.putLong(FIRST_ENTRY_REQUEST_TIMESTAMP, firstEntryRequestTimestamp);
104         bundle.putBoolean(RE_CONSENT_STATUS, reConsentStatus);
105 
106         final JobInfo job =
107                 new JobInfo.Builder(
108                                 CONSENT_NOTIFICATION_JOB_ID,
109                                 new ComponentName(context, ConsentNotificationJobService.class))
110                         .setMinimumLatency(initialDelay)
111                         .setOverrideDeadline(deadline)
112                         .setExtras(bundle)
113                         .setPersisted(true)
114                         .build();
115         jobScheduler.schedule(job);
116         LogUtil.d("Scheduling Consent notification job ...");
117     }
118 
calculateInitialDelay(Calendar calendar)119     static long calculateInitialDelay(Calendar calendar) {
120         Flags flags = FlagsFactory.getFlags();
121         if (flags.getConsentNotificationDebugMode()) {
122             LogUtil.d("Debug mode is enabled. Setting initial delay to 0");
123             return 0L;
124         }
125         long millisecondsInTheCurrentDay = getMillisecondsInTheCurrentDay(calendar);
126 
127         // If the current time (millisecondsInTheCurrentDay) is before
128         // ConsentNotificationIntervalBeginMs (by default 9AM), schedule a job the same day at
129         // earliest (ConsentNotificationIntervalBeginMs).
130         if (millisecondsInTheCurrentDay < flags.getConsentNotificationIntervalBeginMs()) {
131             return flags.getConsentNotificationIntervalBeginMs() - millisecondsInTheCurrentDay;
132         }
133 
134         // If the current time (millisecondsInTheCurrentDay) is in the interval:
135         // (ConsentNotificationIntervalBeginMs, ConsentNotificationIntervalEndMs) schedule
136         // a job ASAP.
137         if (millisecondsInTheCurrentDay >= flags.getConsentNotificationIntervalBeginMs()
138                 && millisecondsInTheCurrentDay
139                         < flags.getConsentNotificationIntervalEndMs()
140                                 - flags.getConsentNotificationMinimalDelayBeforeIntervalEnds()) {
141             return 0L;
142         }
143 
144         // If the current time (millisecondsInTheCurrentDay) is after
145         // ConsentNotificationIntervalEndMs (by default 5 PM) schedule a job the following day at
146         // ConsentNotificationIntervalBeginMs (by default 9AM).
147         return MILLISECONDS_IN_THE_DAY
148                 - millisecondsInTheCurrentDay
149                 + flags.getConsentNotificationIntervalBeginMs();
150     }
151 
calculateDeadline(Calendar calendar)152     static long calculateDeadline(Calendar calendar) {
153         Flags flags = FlagsFactory.getFlags();
154         if (flags.getConsentNotificationDebugMode()) {
155             LogUtil.d("Debug mode is enabled. Setting initial delay to 0");
156             return 0L;
157         }
158 
159         long millisecondsInTheCurrentDay = getMillisecondsInTheCurrentDay(calendar);
160 
161         // If the current time (millisecondsInTheCurrentDay) is before
162         // ConsentNotificationIntervalEndMs (by default 5PM) reduced by
163         // ConsentNotificationMinimalDelayBeforeIntervalEnds (offset period - default 1 hour) set
164         // a deadline for the ConsentNotificationIntervalEndMs the same day.
165         if (millisecondsInTheCurrentDay
166                 < flags.getConsentNotificationIntervalEndMs()
167                         - flags.getConsentNotificationMinimalDelayBeforeIntervalEnds()) {
168             return flags.getConsentNotificationIntervalEndMs() - millisecondsInTheCurrentDay;
169         }
170 
171         // Otherwise, set a deadline for the ConsentNotificationIntervalEndMs the following day.
172         return MILLISECONDS_IN_THE_DAY
173                 - millisecondsInTheCurrentDay
174                 + flags.getConsentNotificationIntervalEndMs();
175     }
176 
getMillisecondsInTheCurrentDay(Calendar calendar)177     private static long getMillisecondsInTheCurrentDay(Calendar calendar) {
178         long currentHour = calendar.get(Calendar.HOUR_OF_DAY);
179         long currentMinute = calendar.get(Calendar.MINUTE);
180         long currentSeconds = calendar.get(Calendar.SECOND);
181         long currentMilliseconds = calendar.get(Calendar.MILLISECOND);
182         long millisecondsInTheCurrentDay = 0;
183 
184         millisecondsInTheCurrentDay += currentHour * 60 * 60 * 1000;
185         millisecondsInTheCurrentDay += currentMinute * 60 * 1000;
186         millisecondsInTheCurrentDay += currentSeconds * 1000;
187         millisecondsInTheCurrentDay += currentMilliseconds;
188 
189         return millisecondsInTheCurrentDay;
190     }
191 
192     /** Set the consent manager instance explicitly (for testing purposes). */
setConsentManager(@onNull ConsentManager consentManager)193     public void setConsentManager(@NonNull ConsentManager consentManager) {
194         mConsentManager = consentManager;
195     }
196 
197     /** Set the ux states manager instance explicitly (for testing purposes). */
setUxStatesManager(@onNull UxStatesManager uxStatesManager)198     public void setUxStatesManager(@NonNull UxStatesManager uxStatesManager) {
199         mUxStatesManager = uxStatesManager;
200     }
201 
202     @Override
onStartJob(JobParameters params)203     public boolean onStartJob(JobParameters params) {
204         // Always ensure that the first thing this job does is check if it should be running, and
205         // cancel itself if it's not supposed to be.
206         if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) {
207             LogUtil.d(
208                     "Disabling ConsentNotificationJobService job because it's running in"
209                             + " ExtServices on T+");
210             return skipAndCancelBackgroundJob(params, /* skipReason= */ 0, /* doRecord= */ false);
211         }
212 
213         LogUtil.d("ConsentNotificationJobService.onStartJob");
214         AdServicesJobServiceLogger.getInstance().recordOnStartJob(CONSENT_NOTIFICATION_JOB_ID);
215 
216         if (mConsentManager == null) {
217             setConsentManager(ConsentManager.getInstance());
218         }
219         if (mUxStatesManager == null) {
220             setUxStatesManager(UxStatesManager.getInstance());
221         }
222 
223         mConsentManager.recordDefaultAdIdState(mConsentManager.isAdIdEnabled());
224         boolean isEeaNotification =
225                 !mConsentManager.isAdIdEnabled() || mUxStatesManager.isEeaDevice();
226         LogUtil.d(
227                 "ConsentNotificationJobService states. isAdIdEnabled: %s, isEeaDevice: %s,"
228                         + " isEeaNotification: %s.",
229                 mConsentManager.isAdIdEnabled(), mUxStatesManager.isEeaDevice(), isEeaNotification);
230         if (mConsentManager.getUx() == RVC_UX) {
231             mConsentManager.recordMeasurementDefaultConsent(!isEeaNotification);
232         } else {
233             mConsentManager.recordDefaultConsent(!isEeaNotification);
234         }
235         boolean reConsentStatus = params.getExtras().getBoolean(RE_CONSENT_STATUS, false);
236 
237         AdServicesExecutors.getBackgroundExecutor()
238                 .execute(
239                         () -> {
240                             try {
241                                 boolean gaUxEnabled =
242                                         FlagsFactory.getFlags().getGaUxFeatureEnabled();
243                                 if (!FlagsFactory.getFlags().getConsentNotificationDebugMode()
244                                         && reConsentStatus
245                                         && !gaUxEnabled) {
246                                     LogUtil.d("already notified, return back");
247                                     return;
248                                 }
249 
250                                 if (FlagsFactory.getFlags().getUiOtaStringsFeatureEnabled()
251                                         || FlagsFactory.getFlags()
252                                                 .getUiOtaResourcesFeatureEnabled()) {
253                                     handleOtaResources(
254                                             params.getExtras()
255                                                     .getLong(
256                                                             FIRST_ENTRY_REQUEST_TIMESTAMP,
257                                                             System.currentTimeMillis()),
258                                             isEeaNotification);
259                                 } else {
260                                     LogUtil.d(
261                                             "OTA strings feature is not enabled, sending"
262                                                     + " notification now.");
263                                     AdServicesSyncUtil.getInstance()
264                                             .execute(this, isEeaNotification);
265                                 }
266                             } finally {
267                                 boolean shouldRetry = false;
268                                 AdServicesJobServiceLogger.getInstance()
269                                         .recordJobFinished(
270                                                 CONSENT_NOTIFICATION_JOB_ID,
271                                                 /* isSuccessful= */ true,
272                                                 shouldRetry);
273 
274                                 jobFinished(params, shouldRetry);
275                             }
276                         });
277         return true;
278     }
279 
280     @Override
onStopJob(JobParameters params)281     public boolean onStopJob(JobParameters params) {
282         LogUtil.d("ConsentNotificationJobService.onStopJob");
283 
284         boolean shouldRetry = true;
285 
286         AdServicesJobServiceLogger.getInstance()
287                 .recordOnStopJob(params, CONSENT_NOTIFICATION_JOB_ID, shouldRetry);
288         return shouldRetry;
289     }
290 
skipAndCancelBackgroundJob( final JobParameters params, int skipReason, boolean doRecord)291     private boolean skipAndCancelBackgroundJob(
292             final JobParameters params, int skipReason, boolean doRecord) {
293         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
294 
295         if (jobScheduler != null) {
296             jobScheduler.cancel(CONSENT_NOTIFICATION_JOB_ID);
297         }
298 
299         if (doRecord) {
300             AdServicesJobServiceLogger.getInstance()
301                     .recordJobSkipped(CONSENT_NOTIFICATION_JOB_ID, skipReason);
302         }
303 
304         // Tell the JobScheduler that the job has completed and does not need to be
305         // rescheduled.
306         jobFinished(params, false);
307 
308         // Returning false means that this job has completed its work.
309         return false;
310     }
311 
handleOtaResources(long firstEntryRequestTimestamp, boolean isEeaNotification)312     private void handleOtaResources(long firstEntryRequestTimestamp, boolean isEeaNotification) {
313         if (System.currentTimeMillis() - firstEntryRequestTimestamp
314                 >= FlagsFactory.getFlags().getUiOtaStringsDownloadDeadline()) {
315             LogUtil.d("Passed OTA resources download deadline, sending" + " notification now.");
316             AdServicesSyncUtil.getInstance().execute(this, isEeaNotification);
317         } else {
318             sendNotificationIfOtaResourcesDownloadCompleted(isEeaNotification);
319         }
320     }
321 
sendNotificationIfOtaResourcesDownloadCompleted(boolean isEeaNotification)322     private void sendNotificationIfOtaResourcesDownloadCompleted(boolean isEeaNotification) {
323         try {
324             ClientFileGroup cfg =
325                     MobileDataDownloadFactory.getMdd(FlagsFactory.getFlags())
326                             .getFileGroup(
327                                     GetFileGroupRequest.newBuilder()
328                                             .setGroupName(
329                                                     FlagsFactory.getFlags()
330                                                             .getUiOtaStringsGroupName())
331                                             .build())
332                             .get();
333             if (cfg != null && cfg.getStatus() == ClientFileGroup.Status.DOWNLOADED) {
334                 LogUtil.d("finished downloading OTA resources." + " Sending notification now.");
335                 AdServicesSyncUtil.getInstance().execute(this, isEeaNotification);
336                 return;
337             }
338         } catch (InterruptedException | ExecutionException e) {
339             LogUtil.e("Error while fetching clientFileGroup: " + e.getMessage());
340             ErrorLogUtil.e(
341                     e,
342                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__LOAD_MDD_FILE_GROUP_FAILURE,
343                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
344         }
345         LogUtil.d("OTA resources are not yet downloaded.");
346         return;
347     }
348 }
349