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 package com.android.adservices.service.measurement.registration; 17 18 import static com.android.adservices.service.measurement.registration.AsyncFetchStatus.EntityStatus; 19 import static com.android.adservices.service.measurement.util.BaseUriExtractor.getBaseUri; 20 import static com.android.adservices.service.measurement.util.MathUtils.extractValidNumberInRange; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENROLLMENT_INVALID; 22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT; 23 24 import android.annotation.NonNull; 25 import android.content.Context; 26 import android.net.Uri; 27 import android.util.Pair; 28 29 import com.android.adservices.LoggerFactory; 30 import com.android.adservices.data.enrollment.EnrollmentDao; 31 import com.android.adservices.data.measurement.DatastoreManager; 32 import com.android.adservices.data.measurement.DatastoreManagerFactory; 33 import com.android.adservices.errorlogging.ErrorLogUtil; 34 import com.android.adservices.service.Flags; 35 import com.android.adservices.service.FlagsFactory; 36 import com.android.adservices.service.common.AllowLists; 37 import com.android.adservices.service.common.WebAddresses; 38 import com.android.adservices.service.measurement.AggregatableNamedBudgets; 39 import com.android.adservices.service.measurement.EventSurfaceType; 40 import com.android.adservices.service.measurement.MeasurementHttpClient; 41 import com.android.adservices.service.measurement.Source; 42 import com.android.adservices.service.measurement.TriggerSpec; 43 import com.android.adservices.service.measurement.TriggerSpecs; 44 import com.android.adservices.service.measurement.countunique.CountUniqueRegistrar; 45 import com.android.adservices.service.measurement.countunique.ICountUniqueRegistrar; 46 import com.android.adservices.service.measurement.reporting.DebugReportApi; 47 import com.android.adservices.service.measurement.util.Enrollment; 48 import com.android.adservices.service.measurement.util.UnsignedLong; 49 import com.android.internal.annotations.VisibleForTesting; 50 51 import org.json.JSONArray; 52 import org.json.JSONException; 53 import org.json.JSONObject; 54 55 import java.io.IOException; 56 import java.io.OutputStream; 57 import java.io.OutputStreamWriter; 58 import java.net.HttpURLConnection; 59 import java.net.MalformedURLException; 60 import java.net.URL; 61 import java.net.URLConnection; 62 import java.nio.charset.StandardCharsets; 63 import java.util.ArrayList; 64 import java.util.Collections; 65 import java.util.HashSet; 66 import java.util.Iterator; 67 import java.util.List; 68 import java.util.Locale; 69 import java.util.Map; 70 import java.util.Optional; 71 import java.util.Set; 72 import java.util.UUID; 73 import java.util.concurrent.TimeUnit; 74 import java.util.stream.Collectors; 75 76 /** 77 * Download and decode Response Based Registration 78 * 79 * @hide 80 */ 81 public class AsyncSourceFetcher { 82 83 private static final long ONE_DAY_IN_SECONDS = TimeUnit.DAYS.toSeconds(1); 84 private static final String DEFAULT_ANDROID_APP_SCHEME = "android-app"; 85 private static final String DEFAULT_ANDROID_APP_URI_PREFIX = DEFAULT_ANDROID_APP_SCHEME + "://"; 86 private final MeasurementHttpClient mNetworkConnection; 87 private final EnrollmentDao mEnrollmentDao; 88 private final Flags mFlags; 89 private final Context mContext; 90 91 private final ICountUniqueRegistrar mCountUniqueRegistrar; 92 private final DatastoreManager mDatastoreManager; 93 private final DebugReportApi mDebugReportApi; 94 AsyncSourceFetcher(Context context)95 public AsyncSourceFetcher(Context context) { 96 this( 97 context, 98 EnrollmentDao.getInstance(), 99 FlagsFactory.getFlags(), 100 new CountUniqueRegistrar(DatastoreManagerFactory.getDatastoreManager()), 101 DatastoreManagerFactory.getDatastoreManager(), 102 new DebugReportApi(context, FlagsFactory.getFlags())); 103 } 104 105 @VisibleForTesting AsyncSourceFetcher( Context context, EnrollmentDao enrollmentDao, Flags flags, ICountUniqueRegistrar countUniqueRegistrar, DatastoreManager datastoreManager, DebugReportApi debugReportApi)106 public AsyncSourceFetcher( 107 Context context, 108 EnrollmentDao enrollmentDao, 109 Flags flags, 110 ICountUniqueRegistrar countUniqueRegistrar, 111 DatastoreManager datastoreManager, 112 DebugReportApi debugReportApi) { 113 mContext = context; 114 mEnrollmentDao = enrollmentDao; 115 mFlags = flags; 116 mNetworkConnection = new MeasurementHttpClient(context); 117 mCountUniqueRegistrar = countUniqueRegistrar; 118 mDatastoreManager = datastoreManager; 119 mDebugReportApi = debugReportApi; 120 } 121 parseValidateSource( String registrationHeaderStr, AsyncRegistration asyncRegistration, Source.Builder builder, String enrollmentId, String sourceId, AsyncFetchStatus asyncFetchStatus)122 private boolean parseValidateSource( 123 String registrationHeaderStr, 124 AsyncRegistration asyncRegistration, 125 Source.Builder builder, 126 String enrollmentId, 127 String sourceId, 128 AsyncFetchStatus asyncFetchStatus) 129 throws JSONException { 130 JSONObject json = new JSONObject(registrationHeaderStr); 131 if (json.isNull(SourceHeaderContract.DESTINATION) 132 && json.isNull(SourceHeaderContract.WEB_DESTINATION)) { 133 LoggerFactory.getMeasurementLogger() 134 .d( 135 "AsyncSourceFetcher: Source destination fields are null or missing. " 136 + "Enrollment ID: %s, Source ID: %s", 137 enrollmentId, sourceId); 138 return false; 139 } 140 long sourceEventTime = asyncRegistration.getRequestTime(); 141 UnsignedLong eventId = new UnsignedLong(0L); 142 if (!json.isNull(SourceHeaderContract.SOURCE_EVENT_ID)) { 143 Optional<UnsignedLong> maybeEventId = 144 FetcherUtil.extractUnsignedLong(json, SourceHeaderContract.SOURCE_EVENT_ID); 145 if (!maybeEventId.isPresent()) { 146 LoggerFactory.getMeasurementLogger() 147 .d( 148 "AsyncSourceFetcher: Invalid %s. " 149 + "Enrollment ID: %s, Source ID: %s", 150 SourceHeaderContract.SOURCE_EVENT_ID, enrollmentId, sourceId); 151 return false; 152 } 153 eventId = maybeEventId.get(); 154 } 155 builder.setEventId(eventId); 156 LoggerFactory.getMeasurementLogger() 157 .d( 158 "AsyncSourceFetcher: Event ID extracted. Validating Source header in" 159 + " registration. Enrollment ID: %s, Source ID: %s, Source Event ID:" 160 + " %s", 161 enrollmentId, sourceId, eventId); 162 long expiry; 163 if (!json.isNull(SourceHeaderContract.EXPIRY)) { 164 UnsignedLong expiryUnsigned = 165 extractValidNumberInRange( 166 new UnsignedLong(json.getString(SourceHeaderContract.EXPIRY)), 167 new UnsignedLong( 168 mFlags 169 .getMeasurementMinReportingRegisterSourceExpirationInSeconds()), 170 new UnsignedLong( 171 mFlags 172 .getMeasurementMaxReportingRegisterSourceExpirationInSeconds())); 173 // Relies on expiryUnsigned not using the 64th bit. 174 expiry = expiryUnsigned.getValue(); 175 if (asyncRegistration.getSourceType() == Source.SourceType.EVENT) { 176 expiry = roundSecondsToWholeDays(expiry); 177 } 178 } else { 179 expiry = mFlags.getMeasurementMaxReportingRegisterSourceExpirationInSeconds(); 180 } 181 builder.setExpiryTime(sourceEventTime + TimeUnit.SECONDS.toMillis(expiry)); 182 long effectiveExpiry = expiry; 183 if (!json.isNull(SourceHeaderContract.EVENT_REPORT_WINDOW)) { 184 long eventReportWindow; 185 UnsignedLong eventReportWindowUnsigned = 186 extractValidNumberInRange( 187 new UnsignedLong( 188 json.getString(SourceHeaderContract.EVENT_REPORT_WINDOW)), 189 new UnsignedLong( 190 mFlags.getMeasurementMinimumEventReportWindowInSeconds()), 191 new UnsignedLong( 192 mFlags 193 .getMeasurementMaxReportingRegisterSourceExpirationInSeconds())); 194 // Relies on eventReportWindowUnsigned not using the 64th bit. 195 eventReportWindow = Math.min(expiry, eventReportWindowUnsigned.getValue()); 196 effectiveExpiry = eventReportWindow; 197 builder.setEventReportWindow(TimeUnit.SECONDS.toMillis(eventReportWindow)); 198 } 199 long aggregateReportWindow; 200 if (!json.isNull(SourceHeaderContract.AGGREGATABLE_REPORT_WINDOW)) { 201 // Registration will be rejected if parsing unsigned long throws. 202 UnsignedLong aggregateReportWindowUnsigned = 203 extractValidNumberInRange( 204 new UnsignedLong( 205 json.getString( 206 SourceHeaderContract.AGGREGATABLE_REPORT_WINDOW)), 207 new UnsignedLong( 208 mFlags 209 .getMeasurementMinimumAggregatableReportWindowInSeconds()), 210 new UnsignedLong( 211 mFlags 212 .getMeasurementMaxReportingRegisterSourceExpirationInSeconds())); 213 // Relies on aggregateReportWindowUnsigned not using the 64th bit. 214 aggregateReportWindow = Math.min(expiry, aggregateReportWindowUnsigned.getValue()); 215 } else { 216 aggregateReportWindow = expiry; 217 } 218 builder.setAggregatableReportWindow( 219 sourceEventTime + TimeUnit.SECONDS.toMillis(aggregateReportWindow)); 220 221 if (!json.isNull(SourceHeaderContract.PRIORITY)) { 222 Optional<Long> maybePriority = 223 FetcherUtil.extractLongString(json, SourceHeaderContract.PRIORITY); 224 if (!maybePriority.isPresent()) { 225 logInvalidSourceField( 226 SourceHeaderContract.PRIORITY, enrollmentId, sourceId, eventId); 227 return false; 228 } 229 builder.setPriority(maybePriority.get()); 230 } 231 232 if (!json.isNull(SourceHeaderContract.DEBUG_REPORTING)) { 233 builder.setIsDebugReporting(json.optBoolean(SourceHeaderContract.DEBUG_REPORTING)); 234 } 235 if (!json.isNull(SourceHeaderContract.DEBUG_KEY)) { 236 Optional<UnsignedLong> maybeDebugKey = 237 FetcherUtil.extractUnsignedLong(json, SourceHeaderContract.DEBUG_KEY); 238 if (maybeDebugKey.isPresent()) { 239 builder.setDebugKey(maybeDebugKey.get()); 240 } 241 } 242 if (!json.isNull(SourceHeaderContract.INSTALL_ATTRIBUTION_WINDOW_KEY)) { 243 long installAttributionWindow = 244 extractValidNumberInRange( 245 json.getLong(SourceHeaderContract.INSTALL_ATTRIBUTION_WINDOW_KEY), 246 mFlags.getMeasurementMinInstallAttributionWindow(), 247 mFlags.getMeasurementMaxInstallAttributionWindow()); 248 builder.setInstallAttributionWindow( 249 TimeUnit.SECONDS.toMillis(installAttributionWindow)); 250 } else { 251 builder.setInstallAttributionWindow( 252 TimeUnit.SECONDS.toMillis(mFlags.getMeasurementMaxInstallAttributionWindow())); 253 } 254 if (!json.isNull(SourceHeaderContract.POST_INSTALL_EXCLUSIVITY_WINDOW_KEY)) { 255 long installCooldownWindow = 256 extractValidNumberInRange( 257 json.getLong(SourceHeaderContract.POST_INSTALL_EXCLUSIVITY_WINDOW_KEY), 258 mFlags.getMeasurementMinPostInstallExclusivityWindow(), 259 mFlags.getMeasurementMaxPostInstallExclusivityWindow()); 260 builder.setInstallCooldownWindow(TimeUnit.SECONDS.toMillis(installCooldownWindow)); 261 } else { 262 builder.setInstallCooldownWindow( 263 TimeUnit.SECONDS.toMillis( 264 mFlags.getMeasurementMinPostInstallExclusivityWindow())); 265 } 266 if (mFlags.getMeasurementEnableReinstallReattribution()) { 267 if (!json.isNull(SourceHeaderContract.REINSTALL_REATTRIBUTION_WINDOW_KEY)) { 268 long reinstallReattributionWindow = 269 extractValidNumberInRange( 270 json.getLong( 271 SourceHeaderContract.REINSTALL_REATTRIBUTION_WINDOW_KEY), 272 0L, 273 mFlags.getMeasurementMaxReinstallReattributionWindowSeconds()); 274 builder.setReinstallReattributionWindow( 275 TimeUnit.SECONDS.toMillis(reinstallReattributionWindow)); 276 } else { 277 builder.setReinstallReattributionWindow(0L); 278 } 279 } 280 // This "filter_data" field is used to generate reports. 281 if (!json.isNull(SourceHeaderContract.FILTER_DATA)) { 282 JSONObject maybeFilterData = json.optJSONObject(SourceHeaderContract.FILTER_DATA); 283 if (maybeFilterData != null && maybeFilterData.has("source_type")) { 284 LoggerFactory.getMeasurementLogger() 285 .d( 286 "AsyncSourceFetcher: Source %s includes 'source_type' key." 287 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 288 SourceHeaderContract.FILTER_DATA, enrollmentId, sourceId, eventId); 289 return false; 290 } 291 if (!FetcherUtil.areValidAttributionFilters( 292 maybeFilterData, 293 mFlags, 294 /* canIncludeLookbackWindow= */ false, 295 /* shouldCheckFilterSize= */ true)) { 296 logInvalidSourceField( 297 SourceHeaderContract.FILTER_DATA, enrollmentId, sourceId, eventId); 298 return false; 299 } 300 builder.setFilterDataString(maybeFilterData.toString()); 301 } 302 303 Uri appUri = null; 304 if (!json.isNull(SourceHeaderContract.DESTINATION)) { 305 appUri = Uri.parse(json.getString(SourceHeaderContract.DESTINATION)); 306 if (appUri.getScheme() == null) { 307 LoggerFactory.getMeasurementLogger() 308 .d( 309 "AsyncSourceFetcher: App %s is missing app scheme, adding." 310 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 311 SourceHeaderContract.DESTINATION, enrollmentId, sourceId, eventId); 312 appUri = Uri.parse(DEFAULT_ANDROID_APP_URI_PREFIX + appUri); 313 } 314 if (!DEFAULT_ANDROID_APP_SCHEME.equals(appUri.getScheme())) { 315 LoggerFactory.getMeasurementLogger() 316 .d( 317 "AsyncSourceFetcher: Invalid scheme for app %s: %s;" 318 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 319 SourceHeaderContract.DESTINATION, 320 appUri.getScheme(), 321 enrollmentId, 322 sourceId, 323 eventId); 324 return false; 325 } 326 } 327 328 String enrollmentBlockList = 329 mFlags.getMeasurementPlatformDebugAdIdMatchingEnrollmentBlocklist(); 330 Set<String> blockedEnrollmentsString = 331 new HashSet<>(AllowLists.splitAllowList(enrollmentBlockList)); 332 if (!AllowLists.doesAllowListAllowAll(enrollmentBlockList) 333 && !blockedEnrollmentsString.contains(enrollmentId) 334 && !json.isNull(SourceHeaderContract.DEBUG_AD_ID)) { 335 builder.setDebugAdId(json.optString(SourceHeaderContract.DEBUG_AD_ID)); 336 } 337 338 Set<String> allowedEnrollmentsString = 339 new HashSet<>( 340 AllowLists.splitAllowList( 341 mFlags.getMeasurementDebugJoinKeyEnrollmentAllowlist())); 342 if (allowedEnrollmentsString.contains(enrollmentId) 343 && !json.isNull(SourceHeaderContract.DEBUG_JOIN_KEY)) { 344 builder.setDebugJoinKey(json.optString(SourceHeaderContract.DEBUG_JOIN_KEY)); 345 } 346 347 if (asyncRegistration.isWebRequest() 348 // Only validate when non-null in request 349 && asyncRegistration.getOsDestination() != null 350 && !asyncRegistration.getOsDestination().equals(appUri)) { 351 LoggerFactory.getMeasurementLogger() 352 .d( 353 "AsyncSourceFetcher: Expected destination to match with the supplied" 354 + " one! Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 355 enrollmentId, sourceId, eventId); 356 return false; 357 } 358 359 if (appUri != null) { 360 builder.setAppDestinations(Collections.singletonList(getBaseUri(appUri))); 361 } 362 363 boolean shouldMatchAtLeastOneWebDestination = 364 asyncRegistration.isWebRequest() && asyncRegistration.getWebDestination() != null; 365 boolean matchedOneWebDestination = false; 366 367 if (!json.isNull(SourceHeaderContract.WEB_DESTINATION)) { 368 Set<Uri> destinationSet = new HashSet<>(); 369 JSONArray jsonDestinations; 370 Object obj = json.get(SourceHeaderContract.WEB_DESTINATION); 371 if (obj instanceof String) { 372 jsonDestinations = new JSONArray(); 373 jsonDestinations.put(json.getString(SourceHeaderContract.WEB_DESTINATION)); 374 } else { 375 jsonDestinations = json.getJSONArray(SourceHeaderContract.WEB_DESTINATION); 376 } 377 if (jsonDestinations.length() 378 > mFlags.getMeasurementMaxDistinctWebDestinationsInSourceRegistration()) { 379 LoggerFactory.getMeasurementLogger() 380 .d( 381 "AsyncSourceFetcher: %s exceeded the limit of %s destinations." 382 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 383 SourceHeaderContract.WEB_DESTINATION, 384 mFlags 385 .getMeasurementMaxDistinctWebDestinationsInSourceRegistration(), 386 enrollmentId, 387 sourceId, 388 eventId); 389 return false; 390 } 391 if (jsonDestinations.length() == 0 && appUri == null) { 392 logInvalidSourceField( 393 SourceHeaderContract.WEB_DESTINATION, enrollmentId, sourceId, eventId); 394 return false; 395 } 396 for (int i = 0; i < jsonDestinations.length(); i++) { 397 Uri destination = Uri.parse(jsonDestinations.getString(i)); 398 if (shouldMatchAtLeastOneWebDestination 399 && asyncRegistration.getWebDestination().equals(destination)) { 400 matchedOneWebDestination = true; 401 } 402 Optional<Uri> topPrivateDomainAndScheme = 403 WebAddresses.topPrivateDomainAndScheme(destination); 404 if (topPrivateDomainAndScheme.isEmpty()) { 405 LoggerFactory.getMeasurementLogger() 406 .d( 407 "AsyncSourceFetcher: Unable to extract top private domain and" 408 + " scheme from web destination. Enrollment ID: %s, Source" 409 + " ID: %s, Source Event ID: %s", 410 enrollmentId, sourceId, eventId); 411 return false; 412 } else { 413 destinationSet.add(topPrivateDomainAndScheme.get()); 414 } 415 } 416 List<Uri> destinationList = new ArrayList<>(destinationSet); 417 if (!destinationList.isEmpty()) { 418 builder.setWebDestinations(destinationList); 419 } 420 } 421 422 if (mFlags.getMeasurementEnableCoarseEventReportDestinations() 423 && !json.isNull(SourceHeaderContract.COARSE_EVENT_REPORT_DESTINATIONS)) { 424 builder.setCoarseEventReportDestinations( 425 json.getBoolean(SourceHeaderContract.COARSE_EVENT_REPORT_DESTINATIONS)); 426 } 427 428 if (shouldMatchAtLeastOneWebDestination && !matchedOneWebDestination) { 429 LoggerFactory.getMeasurementLogger() 430 .d( 431 "AsyncSourceFetcher: Expected at least one of %s to match with the" 432 + " supplied one! Enrollment ID: %s, Source ID: %s, Source Event" 433 + " ID: %s", 434 SourceHeaderContract.WEB_DESTINATION, enrollmentId, sourceId, eventId); 435 return false; 436 } 437 438 Source.TriggerDataMatching triggerDataMatching = Source.TriggerDataMatching.MODULUS; 439 440 if (mFlags.getMeasurementEnableTriggerDataMatching() 441 && !json.isNull(SourceHeaderContract.TRIGGER_DATA_MATCHING)) { 442 // If the token for trigger_data_matching is not in the predefined list, it will 443 // throw IllegalArgumentException that will be caught by the overall parser. 444 triggerDataMatching = 445 Source.TriggerDataMatching.valueOf( 446 json.getString(SourceHeaderContract.TRIGGER_DATA_MATCHING) 447 .toUpperCase(Locale.ENGLISH)); 448 builder.setTriggerDataMatching(triggerDataMatching); 449 } 450 451 JSONObject eventReportWindows = null; 452 Integer maxEventLevelReports = null; 453 if (!json.isNull(SourceHeaderContract.MAX_EVENT_LEVEL_REPORTS)) { 454 Object maxEventLevelReportsObj = json.get(SourceHeaderContract.MAX_EVENT_LEVEL_REPORTS); 455 maxEventLevelReports = json.getInt(SourceHeaderContract.MAX_EVENT_LEVEL_REPORTS); 456 if (!FetcherUtil.is64BitInteger(maxEventLevelReportsObj) 457 || maxEventLevelReports < 0 458 || maxEventLevelReports > mFlags.getMeasurementFlexApiMaxEventReports()) { 459 logInvalidSourceField( 460 SourceHeaderContract.MAX_EVENT_LEVEL_REPORTS, 461 enrollmentId, 462 sourceId, 463 eventId); 464 return false; 465 } 466 builder.setMaxEventLevelReports(maxEventLevelReports); 467 } 468 469 if (!json.isNull(SourceHeaderContract.EVENT_REPORT_WINDOWS)) { 470 if (!json.isNull(SourceHeaderContract.EVENT_REPORT_WINDOW)) { 471 LoggerFactory.getMeasurementLogger() 472 .d( 473 "AsyncSourceFetcher: Only one of %s and %s is expected." 474 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 475 SourceHeaderContract.EVENT_REPORT_WINDOW, 476 SourceHeaderContract.EVENT_REPORT_WINDOWS, 477 enrollmentId, 478 sourceId, 479 eventId); 480 return false; 481 } 482 Optional<JSONObject> maybeEventReportWindows = 483 getValidEventReportWindows( 484 new JSONObject( 485 json.getString(SourceHeaderContract.EVENT_REPORT_WINDOWS)), 486 expiry); 487 if (!maybeEventReportWindows.isPresent()) { 488 logInvalidSourceField( 489 SourceHeaderContract.EVENT_REPORT_WINDOWS, enrollmentId, sourceId, eventId); 490 return false; 491 } 492 eventReportWindows = maybeEventReportWindows.get(); 493 builder.setEventReportWindows(eventReportWindows.toString()); 494 } 495 496 if (mFlags.getMeasurementEnableV1SourceTriggerData() 497 && !json.isNull(SourceHeaderContract.TRIGGER_DATA)) { 498 if (!json.isNull(SourceHeaderContract.TRIGGER_SPECS)) { 499 LoggerFactory.getMeasurementLogger() 500 .d( 501 "AsyncSourceFetcher: Only one of %s or %s is expected." 502 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 503 SourceHeaderContract.TRIGGER_DATA, 504 SourceHeaderContract.TRIGGER_SPECS, 505 enrollmentId, 506 sourceId, 507 eventId); 508 return false; 509 } 510 // Validate input type 511 Optional<JSONArray> maybeTriggerDataListJson = 512 extractLongJsonArray(json, TriggerSpecs.FlexEventReportJsonKeys.TRIGGER_DATA); 513 if (maybeTriggerDataListJson.isEmpty()) { 514 LoggerFactory.getMeasurementLogger() 515 .d( 516 "AsyncSourceFetcher: Expected %s list to contain Longs. " 517 + "Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 518 TriggerSpecs.FlexEventReportJsonKeys.TRIGGER_DATA, 519 enrollmentId, 520 sourceId, 521 eventId); 522 return false; 523 } 524 525 List<UnsignedLong> triggerDataList = 526 TriggerSpec.getTriggerDataArrayFromJson(maybeTriggerDataListJson.get()); 527 Set<UnsignedLong> triggerDataSet = new HashSet<>(); 528 529 // Validate unique trigger data and their magnitude 530 Optional<Set<UnsignedLong>> maybeTriggerDataSet = 531 populateAndValidateTriggerDataSet(triggerDataSet, triggerDataList); 532 if (maybeTriggerDataSet.isEmpty()) { 533 LoggerFactory.getMeasurementLogger() 534 .d( 535 "AsyncSourceFetcher: Invalid or duplicate value in %s list." 536 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 537 TriggerSpecs.FlexEventReportJsonKeys.TRIGGER_DATA, 538 enrollmentId, 539 sourceId, 540 eventId); 541 return false; 542 } 543 // Validate overall set size and contiguity if matching is modulus 544 if (!isValidTriggerDataSet(triggerDataSet, triggerDataMatching)) { 545 LoggerFactory.getMeasurementLogger() 546 .d( 547 "AsyncSourceFetcher: Invalid %s list size or contiguity." 548 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 549 TriggerSpecs.FlexEventReportJsonKeys.TRIGGER_DATA, 550 enrollmentId, 551 sourceId, 552 eventId); 553 return false; 554 } 555 builder.setTriggerData(triggerDataSet); 556 } 557 558 if (mFlags.getMeasurementFlexibleEventReportingApiEnabled() 559 && !json.isNull(SourceHeaderContract.TRIGGER_SPECS)) { 560 if (!json.isNull(SourceHeaderContract.TRIGGER_DATA)) { 561 LoggerFactory.getMeasurementLogger() 562 .d( 563 "AsyncSourceFetcher: Only one of %s or %s is expected." 564 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 565 SourceHeaderContract.TRIGGER_DATA, 566 SourceHeaderContract.TRIGGER_SPECS, 567 enrollmentId, 568 sourceId, 569 eventId); 570 return false; 571 } 572 573 String triggerSpecString = json.getString(SourceHeaderContract.TRIGGER_SPECS); 574 575 final int finalMaxEventLevelReports = 576 Source.getOrDefaultMaxEventLevelReports( 577 asyncRegistration.getSourceType(), maxEventLevelReports, mFlags); 578 579 Optional<TriggerSpec[]> maybeTriggerSpecArray = 580 getValidTriggerSpecs( 581 triggerSpecString, 582 eventReportWindows, 583 effectiveExpiry, 584 asyncRegistration.getSourceType(), 585 finalMaxEventLevelReports, 586 triggerDataMatching); 587 588 if (!maybeTriggerSpecArray.isPresent()) { 589 logInvalidSourceField( 590 SourceHeaderContract.TRIGGER_SPECS, enrollmentId, sourceId, eventId); 591 return false; 592 } 593 594 builder.setTriggerSpecs( 595 new TriggerSpecs(maybeTriggerSpecArray.get(), finalMaxEventLevelReports, null)); 596 } 597 598 if (mFlags.getMeasurementEnableSharedSourceDebugKey() 599 && !json.isNull(SourceHeaderContract.SHARED_DEBUG_KEY)) { 600 try { 601 builder.setSharedDebugKey( 602 new UnsignedLong(json.getString(SourceHeaderContract.SHARED_DEBUG_KEY))); 603 } catch (NumberFormatException e) { 604 LoggerFactory.getMeasurementLogger() 605 .d( 606 e, 607 "AsyncSourceFetcher: parsing %s failed, continuing to parse source." 608 + "Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 609 SourceHeaderContract.SHARED_DEBUG_KEY, 610 enrollmentId, 611 sourceId, 612 eventId); 613 } 614 } 615 616 if (mFlags.getMeasurementEnableAttributionScope() 617 && !json.isNull(SourceHeaderContract.ATTRIBUTION_SCOPES) 618 && !populateAttributionScopeFields(json, builder)) { 619 logInvalidSourceField( 620 SourceHeaderContract.ATTRIBUTION_SCOPES, enrollmentId, sourceId, eventId); 621 return false; 622 } 623 624 if (mFlags.getMeasurementEnableSourceDestinationLimitPriority() 625 && !json.isNull(SourceHeaderContract.DESTINATION_LIMIT_PRIORITY)) { 626 Optional<Long> destinationLimitPriority = 627 FetcherUtil.extractLongString( 628 json, SourceHeaderContract.DESTINATION_LIMIT_PRIORITY); 629 if (destinationLimitPriority.isEmpty()) { 630 LoggerFactory.getMeasurementLogger() 631 .d( 632 "AsyncSourceFetcher: Expected %s to be a Long String. Enrollment" 633 + " ID: %s, Source ID: %s, Source Event ID: %s", 634 SourceHeaderContract.DESTINATION_LIMIT_PRIORITY, 635 enrollmentId, 636 sourceId, 637 eventId); 638 return false; 639 } 640 builder.setDestinationLimitPriority(destinationLimitPriority.get()); 641 } 642 643 if (mFlags.getMeasurementEnableSourceDestinationLimitAlgorithmField()) { 644 if (json.isNull(SourceHeaderContract.DESTINATION_LIMIT_ALGORITHM)) { 645 builder.setDestinationLimitAlgorithm( 646 Source.DestinationLimitAlgorithm.values()[ 647 mFlags.getMeasurementDefaultSourceDestinationLimitAlgorithm()]); 648 } else { 649 String destinationLimitAlgorithm = 650 json.getString(SourceHeaderContract.DESTINATION_LIMIT_ALGORITHM) 651 .toUpperCase(); 652 builder.setDestinationLimitAlgorithm( 653 Source.DestinationLimitAlgorithm.valueOf(destinationLimitAlgorithm)); 654 } 655 } 656 if (!json.isNull(SourceHeaderContract.AGGREGATION_KEYS)) { 657 if (!areValidAggregationKeys( 658 json.getJSONObject(SourceHeaderContract.AGGREGATION_KEYS))) { 659 logInvalidSourceField( 660 SourceHeaderContract.AGGREGATION_KEYS, enrollmentId, sourceId, eventId); 661 return false; 662 } 663 builder.setAggregateSource(json.getString(SourceHeaderContract.AGGREGATION_KEYS)); 664 } 665 if (mFlags.getMeasurementEnableXNA() 666 && !json.isNull(SourceHeaderContract.SHARED_AGGREGATION_KEYS)) { 667 // Parsed as JSONArray for validation 668 JSONArray sharedAggregationKeys = 669 json.getJSONArray(SourceHeaderContract.SHARED_AGGREGATION_KEYS); 670 builder.setSharedAggregationKeys(sharedAggregationKeys.toString()); 671 } 672 if (mFlags.getMeasurementEnableSharedFilterDataKeysXNA() 673 && !json.isNull(SourceHeaderContract.SHARED_FILTER_DATA_KEYS)) { 674 // Parsed as JSONArray for validation 675 JSONArray sharedFilterDataKeys = 676 json.getJSONArray(SourceHeaderContract.SHARED_FILTER_DATA_KEYS); 677 builder.setSharedFilterDataKeys(sharedFilterDataKeys.toString()); 678 } 679 if (mFlags.getMeasurementEnablePreinstallCheck() 680 && !json.isNull(SourceHeaderContract.DROP_SOURCE_IF_INSTALLED)) { 681 builder.setDropSourceIfInstalled( 682 json.getBoolean(SourceHeaderContract.DROP_SOURCE_IF_INSTALLED)); 683 } 684 if (mFlags.getMeasurementEnableEventLevelEpsilonInSource()) { 685 if (!json.isNull(SourceHeaderContract.EVENT_LEVEL_EPSILON)) { 686 Object eventLevelEpsilon = json.get(SourceHeaderContract.EVENT_LEVEL_EPSILON); 687 Optional<Double> validEventLevelEpsilon = 688 validateAndGetEventLevelEpsilon(eventLevelEpsilon); 689 if (validEventLevelEpsilon.isEmpty()) { 690 logInvalidSourceField( 691 SourceHeaderContract.EVENT_LEVEL_EPSILON, 692 enrollmentId, 693 sourceId, 694 eventId); 695 return false; 696 } 697 asyncFetchStatus.setIsEventLevelEpsilonConfigured(true); 698 builder.setEventLevelEpsilon(validEventLevelEpsilon.get()); 699 } else { 700 builder.setEventLevelEpsilon((double) mFlags.getMeasurementPrivacyEpsilon()); 701 } 702 } 703 if (mFlags.getMeasurementEnableAggregateDebugReporting() 704 && !json.isNull(SourceHeaderContract.AGGREGATABLE_DEBUG_REPORTING)) { 705 Optional<String> validAggregateDebugReporting = 706 FetcherUtil.getValidAggregateDebugReportingWithBudget( 707 json.getJSONObject(SourceHeaderContract.AGGREGATABLE_DEBUG_REPORTING), 708 mFlags); 709 if (validAggregateDebugReporting.isPresent()) { 710 builder.setAggregateDebugReportingString(validAggregateDebugReporting.get()); 711 } else { 712 LoggerFactory.getMeasurementLogger() 713 .d( 714 "AsyncSourceFetcher: Invalid %s, continuing to parse source." 715 + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 716 SourceHeaderContract.AGGREGATABLE_DEBUG_REPORTING, 717 enrollmentId, 718 sourceId, 719 eventId); 720 } 721 } 722 if (mFlags.getMeasurementEnableAggregatableNamedBudgets() 723 && !json.isNull(SourceHeaderContract.NAMED_BUDGETS)) { 724 Optional<AggregatableNamedBudgets> maybeAggregatableNamedBudgets = 725 parseAggregatableNamedBudgets( 726 json.getJSONObject(SourceHeaderContract.NAMED_BUDGETS)); 727 if (maybeAggregatableNamedBudgets.isEmpty()) { 728 logInvalidSourceField( 729 SourceHeaderContract.NAMED_BUDGETS, enrollmentId, sourceId, eventId); 730 return false; 731 } 732 builder.setAggregatableNamedBudgets(maybeAggregatableNamedBudgets.get()); 733 } 734 return true; 735 } 736 737 // Populates attribution scope fields if they are available. 738 // Returns false if the json fields are invalid. 739 // Note returning true doesn't indicate whether the fields are populated or not. populateAttributionScopeFields(JSONObject parentJson, Source.Builder builder)740 private boolean populateAttributionScopeFields(JSONObject parentJson, Source.Builder builder) 741 throws JSONException { 742 JSONObject json = parentJson.getJSONObject(SourceHeaderContract.ATTRIBUTION_SCOPES); 743 if (json.isNull(SourceHeaderContract.ATTRIBUTION_SCOPE_LIMIT) 744 || json.isNull(SourceHeaderContract.ATTRIBUTION_SCOPES_VALUES)) { 745 LoggerFactory.getMeasurementLogger() 746 .e("Attribution scope limit and values should be set for attribution scopes"); 747 return false; 748 } 749 750 // Parses attribution scope limit. 751 Optional<Long> maybeAttributionScopeLimit = 752 FetcherUtil.extractLong(json, SourceHeaderContract.ATTRIBUTION_SCOPE_LIMIT); 753 if (maybeAttributionScopeLimit.isEmpty()) { 754 return false; 755 } 756 long attributionScopeLimit = maybeAttributionScopeLimit.get(); 757 758 // Parses attribution scopes values. 759 Optional<List<String>> maybeAttributionScopes = 760 FetcherUtil.extractStringArray( 761 json, 762 SourceHeaderContract.ATTRIBUTION_SCOPES_VALUES, 763 mFlags.getMeasurementMaxAttributionScopesPerSource(), 764 mFlags.getMeasurementMaxAttributionScopeLength()); 765 if (maybeAttributionScopes.isEmpty()) { 766 return false; 767 } 768 List<String> attributionScopes = maybeAttributionScopes.get(); 769 770 // Parses max event states, can be optional, fallback to default max event states. 771 long maxEventStates = Source.DEFAULT_MAX_EVENT_STATES; 772 if (!json.isNull(SourceHeaderContract.MAX_EVENT_STATES)) { 773 Optional<Long> maybeMaxEventStates = 774 FetcherUtil.extractLong(json, SourceHeaderContract.MAX_EVENT_STATES); 775 if (maybeMaxEventStates.isEmpty()) { 776 return false; 777 } 778 if (maybeMaxEventStates.get() <= 0 779 || maybeMaxEventStates.get() 780 > mFlags.getMeasurementMaxReportStatesPerSourceRegistration()) { 781 LoggerFactory.getMeasurementLogger() 782 .e( 783 "Max event states should be a positive integer and smaller than max" 784 + " report states per source registration."); 785 return false; 786 } 787 maxEventStates = maybeMaxEventStates.get(); 788 } 789 790 if (attributionScopeLimit <= 0 || attributionScopes.size() > attributionScopeLimit) { 791 LoggerFactory.getMeasurementLogger() 792 .e( 793 "Attribution scope limit should be positive and not be smaller " 794 + "than the number of attribution scopes."); 795 return false; 796 } 797 if (attributionScopes.isEmpty()) { 798 LoggerFactory.getMeasurementLogger() 799 .e( 800 "Attribution scopes should not be empty if attribution scope limit is" 801 + " set."); 802 return false; 803 } 804 805 builder.setAttributionScopeLimit(attributionScopeLimit); 806 builder.setAttributionScopes(attributionScopes); 807 builder.setMaxEventStates(maxEventStates); 808 return true; 809 } 810 parseAggregatableNamedBudgets( JSONObject namedBudgetObj)811 private Optional<AggregatableNamedBudgets> parseAggregatableNamedBudgets( 812 JSONObject namedBudgetObj) { 813 if (namedBudgetObj.length() > mFlags.getMeasurementMaxNamedBudgetsPerSourceRegistration()) { 814 LoggerFactory.getMeasurementLogger() 815 .d( 816 "parseAggregatableNamedBudgets: more named budgets than permitted. %s", 817 namedBudgetObj.length()); 818 return Optional.empty(); 819 } 820 AggregatableNamedBudgets aggregatableNamedBudgets = new AggregatableNamedBudgets(); 821 822 Iterator<String> keys = namedBudgetObj.keys(); 823 while (keys.hasNext()) { 824 String name = keys.next(); 825 if (name.length() > mFlags.getMeasurementMaxLengthPerBudgetName()) { 826 LoggerFactory.getMeasurementLogger() 827 .d("parseAggregatableNamedBudgets: budget name is invalid." + " %s", name); 828 return Optional.empty(); 829 } 830 Optional<Integer> maybeIntBudget = FetcherUtil.extractIntegralInt(namedBudgetObj, name); 831 if (maybeIntBudget.isEmpty()) { 832 LoggerFactory.getMeasurementLogger() 833 .d("parseAggregatableNamedBudgets: budget isn't an integer. %s", name); 834 return Optional.empty(); 835 } 836 int intBudget = maybeIntBudget.get(); 837 if (intBudget < 0) { 838 LoggerFactory.getMeasurementLogger() 839 .d("parseAggregatableNamedBudgets: budget is negative. %s", intBudget); 840 return Optional.empty(); 841 } 842 if (intBudget > mFlags.getMeasurementMaxSumOfAggregateValuesPerSource()) { 843 LoggerFactory.getMeasurementLogger() 844 .d( 845 "parseAggregatableNamedBudgets: budget is over max capacity. %s", 846 intBudget); 847 return Optional.empty(); 848 } 849 850 aggregatableNamedBudgets.createContributionBudget(name, intBudget); 851 } 852 853 return Optional.of(aggregatableNamedBudgets); 854 } 855 isValidTriggerDataSet(Set<UnsignedLong> triggerDataSet, Source.TriggerDataMatching triggerDataMatching)856 private boolean isValidTriggerDataSet(Set<UnsignedLong> triggerDataSet, 857 Source.TriggerDataMatching triggerDataMatching) { 858 if (triggerDataSet.size() > mFlags.getMeasurementFlexApiMaxTriggerDataCardinality()) { 859 return false; 860 } 861 if (mFlags.getMeasurementEnableTriggerDataMatching() 862 && triggerDataMatching == Source.TriggerDataMatching.MODULUS 863 && !isContiguousStartingAtZero(triggerDataSet)) { 864 return false; 865 } 866 return true; 867 } 868 getValidTriggerSpecs( String triggerSpecString, JSONObject eventReportWindows, long expiry, Source.SourceType sourceType, int maxEventLevelReports, Source.TriggerDataMatching triggerDataMatching)869 private Optional<TriggerSpec[]> getValidTriggerSpecs( 870 String triggerSpecString, 871 JSONObject eventReportWindows, 872 long expiry, 873 Source.SourceType sourceType, 874 int maxEventLevelReports, 875 Source.TriggerDataMatching triggerDataMatching) { 876 List<Pair<Long, Long>> parsedEventReportWindows = 877 Source.getOrDefaultEventReportWindowsForFlex( 878 eventReportWindows, sourceType, TimeUnit.SECONDS.toMillis(expiry), mFlags); 879 long defaultStart = parsedEventReportWindows.get(0).first; 880 List<Long> defaultEnds = 881 parsedEventReportWindows.stream().map((x) -> x.second).collect(Collectors.toList()); 882 try { 883 JSONArray triggerSpecArray = new JSONArray(triggerSpecString); 884 TriggerSpec[] validTriggerSpecs = new TriggerSpec[triggerSpecArray.length()]; 885 Set<UnsignedLong> triggerDataSet = new HashSet<>(); 886 for (int i = 0; i < triggerSpecArray.length(); i++) { 887 Optional<TriggerSpec> maybeTriggerSpec = getValidTriggerSpec( 888 triggerSpecArray.getJSONObject(i), 889 expiry, 890 defaultStart, 891 defaultEnds, 892 triggerDataSet, 893 maxEventLevelReports); 894 if (!maybeTriggerSpec.isPresent()) { 895 return Optional.empty(); 896 } 897 validTriggerSpecs[i] = maybeTriggerSpec.get(); 898 } 899 if (!isValidTriggerDataSet(triggerDataSet, triggerDataMatching)) { 900 return Optional.empty(); 901 } 902 return Optional.of(validTriggerSpecs); 903 } catch (JSONException | IllegalArgumentException ex) { 904 LoggerFactory.getMeasurementLogger().d(ex, "Trigger Spec parsing failed"); 905 return Optional.empty(); 906 } 907 } 908 populateAndValidateTriggerDataSet( Set<UnsignedLong> triggerDataSet, List<UnsignedLong> triggerDataList)909 private static Optional<Set<UnsignedLong>> populateAndValidateTriggerDataSet( 910 Set<UnsignedLong> triggerDataSet, List<UnsignedLong> triggerDataList) { 911 // Check exclusivity of trigger_data across the whole trigger spec array, and validate 912 // trigger data magnitude. 913 for (UnsignedLong triggerData : triggerDataList) { 914 if (!triggerDataSet.add(triggerData) 915 || triggerData.compareTo(TriggerSpecs.MAX_TRIGGER_DATA_VALUE) > 0) { 916 return Optional.empty(); 917 } 918 } 919 return Optional.of(triggerDataSet); 920 } 921 getValidTriggerSpec( JSONObject triggerSpecJson, long expiry, long defaultStart, List<Long> defaultEnds, Set<UnsignedLong> triggerDataSet, int maxEventLevelReports)922 private Optional<TriggerSpec> getValidTriggerSpec( 923 JSONObject triggerSpecJson, 924 long expiry, 925 long defaultStart, 926 List<Long> defaultEnds, 927 Set<UnsignedLong> triggerDataSet, 928 int maxEventLevelReports) throws JSONException { 929 Optional<JSONArray> maybeTriggerDataListJson = extractLongJsonArray( 930 triggerSpecJson, TriggerSpecs.FlexEventReportJsonKeys.TRIGGER_DATA); 931 if (maybeTriggerDataListJson.isEmpty()) { 932 return Optional.empty(); 933 } 934 935 List<UnsignedLong> triggerDataList = 936 TriggerSpec.getTriggerDataArrayFromJson(maybeTriggerDataListJson.get()); 937 if (triggerDataList.isEmpty()) { 938 return Optional.empty(); 939 } 940 941 Optional<Set<UnsignedLong>> maybeTriggerDataSet = populateAndValidateTriggerDataSet( 942 triggerDataSet, triggerDataList); 943 if (maybeTriggerDataSet.isEmpty()) { 944 return Optional.empty(); 945 } 946 947 if (!triggerSpecJson.isNull(TriggerSpecs.FlexEventReportJsonKeys.EVENT_REPORT_WINDOWS)) { 948 Optional<JSONObject> maybeEventReportWindows = 949 getValidEventReportWindows( 950 triggerSpecJson.getJSONObject( 951 TriggerSpecs.FlexEventReportJsonKeys.EVENT_REPORT_WINDOWS), 952 expiry); 953 if (!maybeEventReportWindows.isPresent()) { 954 return Optional.empty(); 955 } 956 } 957 958 TriggerSpec.SummaryOperatorType summaryWindowOperator = 959 TriggerSpec.SummaryOperatorType.COUNT; 960 if (!triggerSpecJson.isNull(TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_OPERATOR)) { 961 // If a summary window operator is not in the predefined list, it will throw 962 // IllegalArgumentException that will be caught by the overall parser. 963 summaryWindowOperator = 964 TriggerSpec.SummaryOperatorType.valueOf( 965 triggerSpecJson 966 .getString( 967 TriggerSpecs.FlexEventReportJsonKeys 968 .SUMMARY_OPERATOR) 969 .toUpperCase(Locale.ENGLISH)); 970 } 971 List<Long> summaryBuckets = null; 972 if (!triggerSpecJson.isNull(TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_BUCKETS)) { 973 Optional<JSONArray> maybeSummaryBucketsJson = extractLongJsonArray( 974 triggerSpecJson, TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_BUCKETS); 975 976 if (maybeSummaryBucketsJson.isEmpty()) { 977 return Optional.empty(); 978 } 979 980 summaryBuckets = TriggerSpec.getLongListFromJson(maybeSummaryBucketsJson.get()); 981 982 if (summaryBuckets.isEmpty() || summaryBuckets.size() > maxEventLevelReports 983 || !TriggerSpec.isStrictIncreasing(summaryBuckets)) { 984 return Optional.empty(); 985 } 986 987 for (Long bucket : summaryBuckets) { 988 if (bucket < 0L || bucket > TriggerSpecs.MAX_BUCKET_THRESHOLD) { 989 return Optional.empty(); 990 } 991 } 992 } 993 994 return Optional.of( 995 new TriggerSpec.Builder( 996 triggerSpecJson, 997 defaultStart, 998 defaultEnds, 999 maxEventLevelReports).build()); 1000 } 1001 getValidEventReportWindows(JSONObject jsonReportWindows, long expiry)1002 private Optional<JSONObject> getValidEventReportWindows(JSONObject jsonReportWindows, 1003 long expiry) throws JSONException { 1004 // Start time in seconds 1005 long startTime = 0; 1006 if (!jsonReportWindows.isNull(TriggerSpecs.FlexEventReportJsonKeys.START_TIME)) { 1007 if (!FetcherUtil.is64BitInteger(jsonReportWindows.get( 1008 TriggerSpecs.FlexEventReportJsonKeys.START_TIME))) { 1009 return Optional.empty(); 1010 } 1011 // We continue to use startTime in seconds for validation but convert it to milliseconds 1012 // for the return JSONObject. 1013 startTime = 1014 jsonReportWindows.getLong(TriggerSpecs.FlexEventReportJsonKeys.START_TIME); 1015 jsonReportWindows.put(TriggerSpecs.FlexEventReportJsonKeys.START_TIME, 1016 TimeUnit.SECONDS.toMillis(startTime)); 1017 } 1018 if (startTime < 0 || startTime > expiry) { 1019 return Optional.empty(); 1020 } 1021 1022 Optional<JSONArray> maybeWindowEndsJson = extractLongJsonArray( 1023 jsonReportWindows, TriggerSpecs.FlexEventReportJsonKeys.END_TIMES); 1024 1025 if (maybeWindowEndsJson.isEmpty()) { 1026 return Optional.empty(); 1027 } 1028 1029 List<Long> windowEnds = TriggerSpec.getLongListFromJson(maybeWindowEndsJson.get()); 1030 1031 int windowEndsSize = windowEnds.size(); 1032 if (windowEnds.isEmpty() 1033 || windowEndsSize > mFlags.getMeasurementFlexApiMaxEventReportWindows()) { 1034 return Optional.empty(); 1035 } 1036 1037 // Clamp last window end to expiry and min event report window. 1038 Long lastWindowsEnd = windowEnds.get(windowEndsSize - 1); 1039 if (lastWindowsEnd < 0) { 1040 return Optional.empty(); 1041 } 1042 windowEnds.set(windowEndsSize - 1, extractValidNumberInRange( 1043 lastWindowsEnd, 1044 mFlags.getMeasurementMinimumEventReportWindowInSeconds(), 1045 expiry)); 1046 1047 if (windowEndsSize > 1) { 1048 // Clamp first window end to min event report window 1049 Long firstWindowsEnd = windowEnds.get(0); 1050 if (firstWindowsEnd < 0) { 1051 return Optional.empty(); 1052 } 1053 windowEnds.set(0, Math.max( 1054 firstWindowsEnd, 1055 mFlags.getMeasurementMinimumEventReportWindowInSeconds())); 1056 } 1057 1058 if (startTime >= windowEnds.get(0) || !TriggerSpec.isStrictIncreasing(windowEnds)) { 1059 return Optional.empty(); 1060 } 1061 1062 jsonReportWindows.put( 1063 TriggerSpecs.FlexEventReportJsonKeys.END_TIMES, 1064 // Convert end times to milliseconds for internal implementation. 1065 new JSONArray(windowEnds.stream().map((x) -> 1066 TimeUnit.SECONDS.toMillis(x)).collect(Collectors.toList()))); 1067 1068 return Optional.of(jsonReportWindows); 1069 } 1070 1071 /** Parse a {@code Source}, given response headers, adding the {@code Source} to a given list */ 1072 @VisibleForTesting parseSource( AsyncRegistration asyncRegistration, String enrollmentId, Map<String, List<String>> headers, AsyncFetchStatus asyncFetchStatus)1073 public Optional<Source> parseSource( 1074 AsyncRegistration asyncRegistration, 1075 String enrollmentId, 1076 Map<String, List<String>> headers, 1077 AsyncFetchStatus asyncFetchStatus) { 1078 boolean arDebugPermission = asyncRegistration.getDebugKeyAllowed(); 1079 LoggerFactory.getMeasurementLogger() 1080 .d("Source ArDebug permission enabled %b", arDebugPermission); 1081 Source.Builder builder = new Source.Builder(); 1082 String sourceId = UUID.randomUUID().toString(); 1083 builder.setId(sourceId); 1084 builder.setRegistrationId(asyncRegistration.getRegistrationId()); 1085 builder.setPublisher(getBaseUri(asyncRegistration.getTopOrigin())); 1086 builder.setEnrollmentId(enrollmentId); 1087 builder.setRegistrant(asyncRegistration.getRegistrant()); 1088 builder.setSourceType(asyncRegistration.getSourceType()); 1089 builder.setAttributionMode(Source.AttributionMode.TRUTHFULLY); 1090 builder.setEventTime(asyncRegistration.getRequestTime()); 1091 builder.setAdIdPermission(asyncRegistration.hasAdIdPermission()); 1092 builder.setArDebugPermission(arDebugPermission); 1093 builder.setPublisherType( 1094 asyncRegistration.isWebRequest() ? EventSurfaceType.WEB : EventSurfaceType.APP); 1095 Optional<Uri> registrationUriOrigin = 1096 WebAddresses.originAndScheme(asyncRegistration.getRegistrationUri()); 1097 LoggerFactory.getMeasurementLogger() 1098 .d( 1099 "AsyncSourceFetcher: Parsing Source. Enrollment ID: %s, Source ID: %s", 1100 enrollmentId, sourceId); 1101 if (!registrationUriOrigin.isPresent()) { 1102 LoggerFactory.getMeasurementLogger() 1103 .d( 1104 "AsyncSourceFetcher: Invalid registration uri. Host: %s, Enrollment" 1105 + " ID: %s, Source ID: %s", 1106 asyncRegistration.getRegistrationUri().getHost(), 1107 enrollmentId, 1108 sourceId); 1109 return Optional.empty(); 1110 } 1111 builder.setRegistrationOrigin(registrationUriOrigin.get()); 1112 builder.setPlatformAdId(asyncRegistration.getPlatformAdId()); 1113 1114 boolean isHeaderErrorDebugReportEnabled = 1115 FetcherUtil.isHeaderErrorDebugReportEnabled( 1116 headers.get(SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_INFO), 1117 mFlags); 1118 String registrationHeaderStr = null; 1119 try { 1120 List<String> field = 1121 headers.get(SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE); 1122 1123 // Check the source registration header size. Only one header is accepted. 1124 if (field == null || field.size() != 1) { 1125 registrationHeaderStr = field == null ? null : field.toString(); 1126 asyncFetchStatus.setEntityStatus(EntityStatus.HEADER_ERROR); 1127 LoggerFactory.getMeasurementLogger() 1128 .d( 1129 String.format( 1130 "AsyncSourceFetcher: Null, empty, or multiple %s headers. " 1131 + "Enrollment ID: %s, Source ID: %s", 1132 SourceHeaderContract 1133 .HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 1134 enrollmentId, 1135 sourceId)); 1136 FetcherUtil.sendHeaderErrorDebugReport( 1137 isHeaderErrorDebugReportEnabled, 1138 mDebugReportApi, 1139 mDatastoreManager, 1140 asyncRegistration.getTopOrigin(), 1141 registrationUriOrigin.get(), 1142 asyncRegistration.getRegistrant(), 1143 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 1144 enrollmentId, 1145 registrationHeaderStr); 1146 return Optional.empty(); 1147 } 1148 1149 // Validate the source header parameters. 1150 registrationHeaderStr = field.get(0); 1151 boolean isValid = 1152 parseValidateSource( 1153 registrationHeaderStr, 1154 asyncRegistration, 1155 builder, 1156 enrollmentId, 1157 sourceId, 1158 asyncFetchStatus); 1159 if (!isValid) { 1160 asyncFetchStatus.setEntityStatus(EntityStatus.VALIDATION_ERROR); 1161 LoggerFactory.getMeasurementLogger() 1162 .d( 1163 String.format( 1164 "AsyncSourceFetcher: Invalid source params in %s header. " 1165 + "Enrollment ID: %s, Source ID: %s", 1166 SourceHeaderContract 1167 .HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 1168 enrollmentId, 1169 sourceId)); 1170 FetcherUtil.sendHeaderErrorDebugReport( 1171 isHeaderErrorDebugReportEnabled, 1172 mDebugReportApi, 1173 mDatastoreManager, 1174 asyncRegistration.getTopOrigin(), 1175 registrationUriOrigin.get(), 1176 asyncRegistration.getRegistrant(), 1177 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 1178 enrollmentId, 1179 registrationHeaderStr); 1180 return Optional.empty(); 1181 } 1182 1183 // Set success status and return parsed source if no error. 1184 asyncFetchStatus.setEntityStatus(EntityStatus.SUCCESS); 1185 return Optional.of(builder.build()); 1186 } catch (JSONException e) { 1187 asyncFetchStatus.setEntityStatus(EntityStatus.PARSING_ERROR); 1188 LoggerFactory.getMeasurementLogger() 1189 .d( 1190 e, 1191 "AsyncSourceFetcher: %s header JSON Parsing Exception. " 1192 + " Enrollment ID: %s, Source ID: %s", 1193 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 1194 enrollmentId, 1195 sourceId); 1196 FetcherUtil.sendHeaderErrorDebugReport( 1197 isHeaderErrorDebugReportEnabled, 1198 mDebugReportApi, 1199 mDatastoreManager, 1200 asyncRegistration.getTopOrigin(), 1201 registrationUriOrigin.get(), 1202 asyncRegistration.getRegistrant(), 1203 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 1204 enrollmentId, 1205 registrationHeaderStr); 1206 return Optional.empty(); 1207 } catch (IllegalArgumentException | ArithmeticException e) { 1208 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.VALIDATION_ERROR); 1209 LoggerFactory.getMeasurementLogger() 1210 .d( 1211 e, 1212 "AsyncSourceFetcher: IllegalArgumentException" 1213 + " or ArithmeticException. Enrollment ID: %s, Source ID: %s", 1214 enrollmentId, 1215 sourceId); 1216 FetcherUtil.sendHeaderErrorDebugReport( 1217 isHeaderErrorDebugReportEnabled, 1218 mDebugReportApi, 1219 mDatastoreManager, 1220 asyncRegistration.getTopOrigin(), 1221 registrationUriOrigin.get(), 1222 asyncRegistration.getRegistrant(), 1223 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 1224 enrollmentId, 1225 registrationHeaderStr); 1226 return Optional.empty(); 1227 } 1228 } 1229 1230 /** Provided a testing hook. */ 1231 @NonNull 1232 @VisibleForTesting openUrl(@onNull URL url)1233 public URLConnection openUrl(@NonNull URL url) throws IOException { 1234 return mNetworkConnection.setup(url); 1235 } 1236 1237 /** 1238 * Fetch a source type registration. 1239 * 1240 * @param asyncRegistration a {@link AsyncRegistration}, a request the record. 1241 * @param asyncFetchStatus a {@link AsyncFetchStatus}, stores Ad Tech server status. 1242 * @param asyncRedirects a {@link AsyncRedirects}, stores redirects. 1243 */ fetchSource( AsyncRegistration asyncRegistration, AsyncFetchStatus asyncFetchStatus, AsyncRedirects asyncRedirects)1244 public Optional<Source> fetchSource( 1245 AsyncRegistration asyncRegistration, 1246 AsyncFetchStatus asyncFetchStatus, 1247 AsyncRedirects asyncRedirects) { 1248 HttpURLConnection urlConnection = null; 1249 Map<String, List<String>> headers; 1250 if (!asyncRegistration.getRegistrationUri().getScheme().equalsIgnoreCase("https")) { 1251 LoggerFactory.getMeasurementLogger() 1252 .d( 1253 "AsyncSourceFetcher: Invalid scheme for registrationUri - %s", 1254 asyncRegistration.getRegistrationUri().getScheme()); 1255 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.INVALID_URL); 1256 return Optional.empty(); 1257 } 1258 // TODO(b/276825561): Fix code duplication between fetchSource & fetchTrigger request flow 1259 try { 1260 urlConnection = 1261 (HttpURLConnection) 1262 openUrl(new URL(asyncRegistration.getRegistrationUri().toString())); 1263 urlConnection.setRequestMethod("POST"); 1264 urlConnection.setRequestProperty( 1265 SourceRequestContract.SOURCE_INFO, 1266 asyncRegistration.getSourceType().getValue()); 1267 urlConnection.setInstanceFollowRedirects(false); 1268 String body = asyncRegistration.getPostBody(); 1269 if (mFlags.getFledgeMeasurementReportAndRegisterEventApiEnabled() && body != null) { 1270 asyncFetchStatus.setPARequestStatus(true); 1271 urlConnection.setRequestProperty("Content-Type", "text/plain"); 1272 urlConnection.setDoOutput(true); 1273 OutputStream os = urlConnection.getOutputStream(); 1274 OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8); 1275 osw.write(body); 1276 osw.flush(); 1277 osw.close(); 1278 } 1279 1280 headers = urlConnection.getHeaderFields(); 1281 asyncFetchStatus.setResponseSize(FetcherUtil.calculateHeadersCharactersLength(headers)); 1282 int responseCode = urlConnection.getResponseCode(); 1283 LoggerFactory.getMeasurementLogger() 1284 .d( 1285 "AsyncSourceFetcher: Response code: %s, Method: %s, Host: %s", 1286 responseCode, 1287 urlConnection.getRequestMethod(), 1288 urlConnection.getURL().getHost()); 1289 if (!FetcherUtil.isRedirect(responseCode) && !FetcherUtil.isSuccess(responseCode)) { 1290 asyncFetchStatus.setResponseStatus( 1291 AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE); 1292 return Optional.empty(); 1293 } 1294 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); 1295 } catch (MalformedURLException e) { 1296 LoggerFactory.getMeasurementLogger() 1297 .e(e, "AsyncSourceFetcher: Malformed registration target URL"); 1298 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.INVALID_URL); 1299 return Optional.empty(); 1300 } catch (IOException e) { 1301 LoggerFactory.getMeasurementLogger() 1302 .e(e, "AsyncSourceFetcher: Failed to get registration response"); 1303 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR); 1304 return Optional.empty(); 1305 } finally { 1306 if (urlConnection != null) { 1307 urlConnection.disconnect(); 1308 } 1309 } 1310 1311 asyncRedirects.configure(headers, asyncRegistration); 1312 1313 if (!isSourceHeaderPresent(headers)) { 1314 asyncFetchStatus.setEntityStatus(EntityStatus.HEADER_MISSING); 1315 asyncFetchStatus.setRedirectOnlyStatus(true); 1316 LoggerFactory.getMeasurementLogger() 1317 .d( 1318 "AsyncSourceFetcher: %s header not found. Host: %s", 1319 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 1320 urlConnection.getURL().getHost()); 1321 return Optional.empty(); 1322 } 1323 1324 Optional<String> enrollmentId = 1325 mFlags.isDisableMeasurementEnrollmentCheck() 1326 ? WebAddresses.topPrivateDomainAndScheme( 1327 asyncRegistration.getRegistrationUri()) 1328 .map(Uri::toString) 1329 : Enrollment.getValidEnrollmentId( 1330 asyncRegistration.getRegistrationUri(), 1331 asyncRegistration.getRegistrant().getAuthority(), 1332 mEnrollmentDao, 1333 mContext, 1334 mFlags); 1335 if (enrollmentId.isEmpty()) { 1336 LoggerFactory.getMeasurementLogger() 1337 .d( 1338 "AsyncSourceFetcher: Enrollment ID could not be verified. Host: %s", 1339 urlConnection.getURL().getHost()); 1340 asyncFetchStatus.setEntityStatus(EntityStatus.INVALID_ENROLLMENT); 1341 ErrorLogUtil.e( 1342 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENROLLMENT_INVALID, 1343 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); 1344 return Optional.empty(); 1345 } 1346 1347 try { 1348 if (isCountUniqueEnabled(asyncRegistration)) { 1349 List<String> metadataHeader = 1350 headers.get(CountUniqueHeaderContract.HEADER_COUNT_UNIQUE_METADATA); 1351 if (metadataHeader != null) { 1352 mCountUniqueRegistrar.registerCountUniqueMetadata( 1353 asyncRegistration, metadataHeader); 1354 } 1355 1356 List<String> eventHeader = 1357 headers.get(CountUniqueHeaderContract.HEADER_COUNT_UNIQUE_EVENT); 1358 if (eventHeader != null) { 1359 mCountUniqueRegistrar.registerCountUniqueEvent( 1360 asyncRegistration, eventHeader, enrollmentId.get()); 1361 } 1362 } 1363 } catch (Exception e) { 1364 // Catching generic exception to not fail ARA source registration flow 1365 // TODO(402862565) Add CEL logging 1366 LoggerFactory.getMeasurementLogger() 1367 .e(e, "AsyncSourceFetcher: Failure when handling count unique header"); 1368 } 1369 return parseSource(asyncRegistration, enrollmentId.get(), headers, asyncFetchStatus); 1370 } 1371 isCountUniqueEnabled(AsyncRegistration asyncRegistration)1372 private boolean isCountUniqueEnabled(AsyncRegistration asyncRegistration) { 1373 return mFlags.getMeasurementEnableCountUniqueService() 1374 && asyncRegistration.isAppRequest() 1375 && AllowLists.isPackageAllowListed( 1376 mFlags.getMeasurementCountUniqueAppAllowlist(), 1377 asyncRegistration.getRegistrant().toString()) 1378 && AllowLists.isSignatureAllowListed( 1379 mContext, 1380 mFlags.getMeasurementCountUniqueAppSignatureAllowlist(), 1381 asyncRegistration.getRegistrant().toString()); 1382 } 1383 isSourceHeaderPresent(Map<String, List<String>> headers)1384 private boolean isSourceHeaderPresent(Map<String, List<String>> headers) { 1385 return headers.containsKey( 1386 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE); 1387 } 1388 validateAndGetEventLevelEpsilon(Object eventLevelEpsilonObj)1389 private Optional<Double> validateAndGetEventLevelEpsilon(Object eventLevelEpsilonObj) { 1390 if (!(eventLevelEpsilonObj instanceof Number)) { 1391 return Optional.empty(); 1392 } 1393 Double validEventLevelEpsilon = (Double) ((Number) eventLevelEpsilonObj).doubleValue(); 1394 if (validEventLevelEpsilon < 0 1395 || validEventLevelEpsilon > mFlags.getMeasurementPrivacyEpsilon()) { 1396 return Optional.empty(); 1397 } 1398 return Optional.of(validEventLevelEpsilon); 1399 } 1400 areValidAggregationKeys(JSONObject aggregationKeys)1401 private boolean areValidAggregationKeys(JSONObject aggregationKeys) { 1402 if (aggregationKeys.length() 1403 > mFlags.getMeasurementMaxAggregateKeysPerSourceRegistration()) { 1404 LoggerFactory.getMeasurementLogger() 1405 .d( 1406 "Aggregation-keys have more entries than permitted. %s", 1407 aggregationKeys.length()); 1408 return false; 1409 } 1410 for (String id : aggregationKeys.keySet()) { 1411 if (!FetcherUtil.isValidAggregateKeyId(id)) { 1412 LoggerFactory.getMeasurementLogger() 1413 .d("SourceFetcher: aggregation key ID is invalid. %s", id); 1414 return false; 1415 } 1416 String keyPiece = aggregationKeys.optString(id); 1417 if (!FetcherUtil.isValidAggregateKeyPiece(keyPiece, mFlags)) { 1418 LoggerFactory.getMeasurementLogger() 1419 .d("SourceFetcher: aggregation key-piece is invalid. %s", keyPiece); 1420 return false; 1421 } 1422 } 1423 return true; 1424 } 1425 isContiguousStartingAtZero(Set<UnsignedLong> unsignedLongs)1426 private static boolean isContiguousStartingAtZero(Set<UnsignedLong> unsignedLongs) { 1427 UnsignedLong upperBound = new UnsignedLong(((long) unsignedLongs.size()) - 1L); 1428 for (UnsignedLong unsignedLong : unsignedLongs) { 1429 if (unsignedLong.compareTo(upperBound) > 0) { 1430 return false; 1431 } 1432 } 1433 return true; 1434 } 1435 extractLongJsonArray(JSONObject json, String key)1436 private static Optional<JSONArray> extractLongJsonArray(JSONObject json, String key) 1437 throws JSONException { 1438 JSONArray jsonArray = json.getJSONArray(key); 1439 for (int i = 0; i < jsonArray.length(); i++) { 1440 if (!FetcherUtil.is64BitInteger(jsonArray.get(i))) { 1441 return Optional.empty(); 1442 } 1443 } 1444 return Optional.of(jsonArray); 1445 } 1446 roundSecondsToWholeDays(long seconds)1447 private static long roundSecondsToWholeDays(long seconds) { 1448 long remainder = seconds % ONE_DAY_IN_SECONDS; 1449 // Return value should be at least one whole day. 1450 boolean roundUp = (remainder >= ONE_DAY_IN_SECONDS / 2L) || (seconds == remainder); 1451 return seconds - remainder + (roundUp ? ONE_DAY_IN_SECONDS : 0); 1452 } 1453 logInvalidSourceField( String field, String enrollmentId, String sourceId, UnsignedLong sourceEventId)1454 private void logInvalidSourceField( 1455 String field, String enrollmentId, String sourceId, UnsignedLong sourceEventId) { 1456 LoggerFactory.getMeasurementLogger() 1457 .d( 1458 "AsyncSourceFetcher: Invalid %s. Enrollment ID: %s, Source ID: %s, Source" 1459 + " Event ID: %s", 1460 field, enrollmentId, sourceId, sourceEventId); 1461 } 1462 1463 private interface SourceHeaderContract { 1464 String HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE = 1465 "Attribution-Reporting-Register-Source"; 1466 // Header for enable header error verbose debug reports. 1467 String HEADER_ATTRIBUTION_REPORTING_INFO = "Attribution-Reporting-Info"; 1468 String SOURCE_EVENT_ID = "source_event_id"; 1469 String DEBUG_KEY = "debug_key"; 1470 String DESTINATION = "destination"; 1471 String EXPIRY = "expiry"; 1472 String EVENT_REPORT_WINDOW = "event_report_window"; 1473 String AGGREGATABLE_REPORT_WINDOW = "aggregatable_report_window"; 1474 String PRIORITY = "priority"; 1475 String INSTALL_ATTRIBUTION_WINDOW_KEY = "install_attribution_window"; 1476 String POST_INSTALL_EXCLUSIVITY_WINDOW_KEY = "post_install_exclusivity_window"; 1477 String REINSTALL_REATTRIBUTION_WINDOW_KEY = "reinstall_reattribution_window"; 1478 String FILTER_DATA = "filter_data"; 1479 String WEB_DESTINATION = "web_destination"; 1480 String AGGREGATION_KEYS = "aggregation_keys"; 1481 String SHARED_AGGREGATION_KEYS = "shared_aggregation_keys"; 1482 String DEBUG_REPORTING = "debug_reporting"; 1483 String DEBUG_JOIN_KEY = "debug_join_key"; 1484 String DEBUG_AD_ID = "debug_ad_id"; 1485 String COARSE_EVENT_REPORT_DESTINATIONS = "coarse_event_report_destinations"; 1486 String TRIGGER_SPECS = "trigger_specs"; 1487 String MAX_EVENT_LEVEL_REPORTS = "max_event_level_reports"; 1488 String EVENT_REPORT_WINDOWS = "event_report_windows"; 1489 String SHARED_DEBUG_KEY = "shared_debug_key"; 1490 String SHARED_FILTER_DATA_KEYS = "shared_filter_data_keys"; 1491 String DROP_SOURCE_IF_INSTALLED = "drop_source_if_installed"; 1492 String TRIGGER_DATA_MATCHING = "trigger_data_matching"; 1493 String TRIGGER_DATA = "trigger_data"; 1494 String ATTRIBUTION_SCOPES = "attribution_scopes"; 1495 String ATTRIBUTION_SCOPE_LIMIT = "limit"; 1496 String ATTRIBUTION_SCOPES_VALUES = "values"; 1497 String MAX_EVENT_STATES = "max_event_states"; 1498 String DESTINATION_LIMIT_PRIORITY = "destination_limit_priority"; 1499 String DESTINATION_LIMIT_ALGORITHM = "destination_limit_algorithm"; 1500 String EVENT_LEVEL_EPSILON = "event_level_epsilon"; 1501 String AGGREGATABLE_DEBUG_REPORTING = "aggregatable_debug_reporting"; 1502 String NAMED_BUDGETS = "named_budgets"; 1503 } 1504 1505 private interface SourceRequestContract { 1506 String SOURCE_INFO = "Attribution-Reporting-Source-Info"; 1507 } 1508 1509 public interface CountUniqueHeaderContract { 1510 String HEADER_COUNT_UNIQUE_EVENT = "Count-Unique-Event"; 1511 String HEADER_COUNT_UNIQUE_METADATA = "Count-Unique-Metadata"; 1512 } 1513 } 1514