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