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