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