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.PrivacyParams.MAX_DISTINCT_WEB_DESTINATIONS_IN_SOURCE_REGISTRATION; 19 import static com.android.adservices.service.measurement.PrivacyParams.MAX_INSTALL_ATTRIBUTION_WINDOW; 20 import static com.android.adservices.service.measurement.PrivacyParams.MAX_POST_INSTALL_EXCLUSIVITY_WINDOW; 21 import static com.android.adservices.service.measurement.PrivacyParams.MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; 22 import static com.android.adservices.service.measurement.PrivacyParams.MIN_INSTALL_ATTRIBUTION_WINDOW; 23 import static com.android.adservices.service.measurement.PrivacyParams.MIN_POST_INSTALL_EXCLUSIVITY_WINDOW; 24 import static com.android.adservices.service.measurement.PrivacyParams.MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; 25 import static com.android.adservices.service.measurement.SystemHealthParams.MAX_AGGREGATE_KEYS_PER_REGISTRATION; 26 import static com.android.adservices.service.measurement.util.BaseUriExtractor.getBaseUri; 27 import static com.android.adservices.service.measurement.util.MathUtils.extractValidNumberInRange; 28 29 import static java.lang.Math.min; 30 31 import android.annotation.NonNull; 32 import android.content.Context; 33 import android.net.Uri; 34 35 import com.android.adservices.LogUtil; 36 import com.android.adservices.data.enrollment.EnrollmentDao; 37 import com.android.adservices.service.Flags; 38 import com.android.adservices.service.FlagsFactory; 39 import com.android.adservices.service.common.AllowLists; 40 import com.android.adservices.service.measurement.EventSurfaceType; 41 import com.android.adservices.service.measurement.MeasurementHttpClient; 42 import com.android.adservices.service.measurement.Source; 43 import com.android.adservices.service.measurement.util.Enrollment; 44 import com.android.adservices.service.measurement.util.UnsignedLong; 45 import com.android.adservices.service.measurement.util.Web; 46 import com.android.adservices.service.stats.AdServicesLogger; 47 import com.android.adservices.service.stats.AdServicesLoggerImpl; 48 import com.android.internal.annotations.VisibleForTesting; 49 50 import org.json.JSONArray; 51 import org.json.JSONException; 52 import org.json.JSONObject; 53 54 import java.io.IOException; 55 import java.net.HttpURLConnection; 56 import java.net.MalformedURLException; 57 import java.net.URL; 58 import java.net.URLConnection; 59 import java.util.ArrayList; 60 import java.util.Collections; 61 import java.util.HashSet; 62 import java.util.List; 63 import java.util.Map; 64 import java.util.Optional; 65 import java.util.Set; 66 import java.util.concurrent.TimeUnit; 67 68 /** 69 * Download and decode Response Based Registration 70 * 71 * @hide 72 */ 73 public class AsyncSourceFetcher { 74 75 private static final long ONE_DAY_IN_SECONDS = TimeUnit.DAYS.toSeconds(1); 76 private static final String DEFAULT_ANDROID_APP_SCHEME = "android-app"; 77 private static final String DEFAULT_ANDROID_APP_URI_PREFIX = DEFAULT_ANDROID_APP_SCHEME + "://"; 78 private final MeasurementHttpClient mNetworkConnection = new MeasurementHttpClient(); 79 private final EnrollmentDao mEnrollmentDao; 80 private final Flags mFlags; 81 private final AdServicesLogger mLogger; 82 private final Context mContext; 83 AsyncSourceFetcher(Context context)84 public AsyncSourceFetcher(Context context) { 85 this( 86 context, 87 EnrollmentDao.getInstance(context), 88 FlagsFactory.getFlags(), 89 AdServicesLoggerImpl.getInstance()); 90 } 91 92 @VisibleForTesting AsyncSourceFetcher( Context context, EnrollmentDao enrollmentDao, Flags flags, AdServicesLogger logger)93 public AsyncSourceFetcher( 94 Context context, EnrollmentDao enrollmentDao, Flags flags, AdServicesLogger logger) { 95 mContext = context; 96 mEnrollmentDao = enrollmentDao; 97 mFlags = flags; 98 mLogger = logger; 99 } 100 parseCommonSourceParams( JSONObject json, AsyncRegistration asyncRegistration, Source.Builder builder, String enrollmentId)101 private boolean parseCommonSourceParams( 102 JSONObject json, 103 AsyncRegistration asyncRegistration, 104 Source.Builder builder, 105 String enrollmentId) 106 throws JSONException { 107 if (!hasRequiredParams(json)) { 108 throw new JSONException( 109 String.format( 110 "Expected %s and a destination", SourceHeaderContract.SOURCE_EVENT_ID)); 111 } 112 long sourceEventTime = asyncRegistration.getRequestTime(); 113 UnsignedLong eventId = new UnsignedLong(0L); 114 if (!json.isNull(SourceHeaderContract.SOURCE_EVENT_ID)) { 115 try { 116 eventId = new UnsignedLong(json.getString(SourceHeaderContract.SOURCE_EVENT_ID)); 117 } catch (NumberFormatException e) { 118 LogUtil.d(e, "parseCommonSourceParams: parsing source_event_id failed."); 119 } 120 } 121 builder.setEventId(eventId); 122 long expiry; 123 if (!json.isNull(SourceHeaderContract.EXPIRY)) { 124 expiry = 125 extractValidNumberInRange( 126 json.getLong(SourceHeaderContract.EXPIRY), 127 MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, 128 MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS); 129 if (asyncRegistration.getSourceType() == Source.SourceType.EVENT) { 130 expiry = roundSecondsToWholeDays(expiry); 131 } 132 } else { 133 expiry = MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; 134 } 135 builder.setExpiryTime( 136 asyncRegistration.getRequestTime() + TimeUnit.SECONDS.toMillis(expiry)); 137 long eventReportWindow; 138 if (!json.isNull(SourceHeaderContract.EVENT_REPORT_WINDOW)) { 139 eventReportWindow = 140 Math.min( 141 expiry, 142 extractValidNumberInRange( 143 json.getLong(SourceHeaderContract.EVENT_REPORT_WINDOW), 144 MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, 145 MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS)); 146 } else { 147 eventReportWindow = expiry; 148 } 149 builder.setEventReportWindow( 150 sourceEventTime + TimeUnit.SECONDS.toMillis(eventReportWindow)); 151 long aggregateReportWindow; 152 if (!json.isNull(SourceHeaderContract.AGGREGATABLE_REPORT_WINDOW)) { 153 aggregateReportWindow = 154 min( 155 expiry, 156 extractValidNumberInRange( 157 json.getLong(SourceHeaderContract.AGGREGATABLE_REPORT_WINDOW), 158 MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, 159 MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS)); 160 } else { 161 aggregateReportWindow = expiry; 162 } 163 builder.setAggregatableReportWindow( 164 sourceEventTime + TimeUnit.SECONDS.toMillis(aggregateReportWindow)); 165 if (!json.isNull(SourceHeaderContract.PRIORITY)) { 166 builder.setPriority(json.getLong(SourceHeaderContract.PRIORITY)); 167 } 168 if (!json.isNull(SourceHeaderContract.DEBUG_REPORTING)) { 169 builder.setIsDebugReporting(json.optBoolean(SourceHeaderContract.DEBUG_REPORTING)); 170 } 171 if (!json.isNull(SourceHeaderContract.DEBUG_KEY)) { 172 try { 173 builder.setDebugKey( 174 new UnsignedLong(json.getString(SourceHeaderContract.DEBUG_KEY))); 175 } catch (NumberFormatException e) { 176 LogUtil.e(e, "parseCommonSourceParams: parsing debug key failed"); 177 } 178 } 179 if (!json.isNull(SourceHeaderContract.INSTALL_ATTRIBUTION_WINDOW_KEY)) { 180 long installAttributionWindow = 181 extractValidNumberInRange( 182 json.getLong(SourceHeaderContract.INSTALL_ATTRIBUTION_WINDOW_KEY), 183 MIN_INSTALL_ATTRIBUTION_WINDOW, 184 MAX_INSTALL_ATTRIBUTION_WINDOW); 185 builder.setInstallAttributionWindow( 186 TimeUnit.SECONDS.toMillis(installAttributionWindow)); 187 } else { 188 builder.setInstallAttributionWindow( 189 TimeUnit.SECONDS.toMillis(MAX_INSTALL_ATTRIBUTION_WINDOW)); 190 } 191 if (!json.isNull(SourceHeaderContract.POST_INSTALL_EXCLUSIVITY_WINDOW_KEY)) { 192 long installCooldownWindow = 193 extractValidNumberInRange( 194 json.getLong(SourceHeaderContract.POST_INSTALL_EXCLUSIVITY_WINDOW_KEY), 195 MIN_POST_INSTALL_EXCLUSIVITY_WINDOW, 196 MAX_POST_INSTALL_EXCLUSIVITY_WINDOW); 197 builder.setInstallCooldownWindow(TimeUnit.SECONDS.toMillis(installCooldownWindow)); 198 } else { 199 builder.setInstallCooldownWindow( 200 TimeUnit.SECONDS.toMillis(MIN_POST_INSTALL_EXCLUSIVITY_WINDOW)); 201 } 202 // This "filter_data" field is used to generate reports. 203 if (!json.isNull(SourceHeaderContract.FILTER_DATA)) { 204 if (!FetcherUtil.areValidAttributionFilters( 205 json.optJSONObject(SourceHeaderContract.FILTER_DATA))) { 206 LogUtil.d("Source filter-data is invalid."); 207 return false; 208 } 209 builder.setFilterData(json.getJSONObject(SourceHeaderContract.FILTER_DATA).toString()); 210 } 211 212 Uri appUri = null; 213 if (!json.isNull(SourceHeaderContract.DESTINATION)) { 214 appUri = Uri.parse(json.getString(SourceHeaderContract.DESTINATION)); 215 if (appUri.getScheme() == null) { 216 LogUtil.d("App destination is missing app scheme, adding."); 217 appUri = Uri.parse(DEFAULT_ANDROID_APP_URI_PREFIX + appUri); 218 } 219 if (!DEFAULT_ANDROID_APP_SCHEME.equals(appUri.getScheme())) { 220 LogUtil.e( 221 "Invalid scheme for app destination: %s; dropping the source.", 222 appUri.getScheme()); 223 return false; 224 } 225 } 226 227 String enrollmentBlockList = 228 mFlags.getMeasurementPlatformDebugAdIdMatchingEnrollmentBlocklist(); 229 Set<String> blockedEnrollmentsString = 230 new HashSet<>(AllowLists.splitAllowList(enrollmentBlockList)); 231 if (!AllowLists.doesAllowListAllowAll(enrollmentBlockList) 232 && !blockedEnrollmentsString.contains(enrollmentId) 233 && !json.isNull(SourceHeaderContract.DEBUG_AD_ID)) { 234 builder.setDebugAdId(json.optString(SourceHeaderContract.DEBUG_AD_ID)); 235 } 236 237 Set<String> allowedEnrollmentsString = 238 new HashSet<>( 239 AllowLists.splitAllowList( 240 mFlags.getMeasurementDebugJoinKeyEnrollmentAllowlist())); 241 if (allowedEnrollmentsString.contains(enrollmentId) 242 && !json.isNull(SourceHeaderContract.DEBUG_JOIN_KEY)) { 243 builder.setDebugJoinKey(json.optString(SourceHeaderContract.DEBUG_JOIN_KEY)); 244 } 245 246 if (asyncRegistration.isWebRequest() 247 // Only validate when non-null in request 248 && asyncRegistration.getOsDestination() != null 249 && !asyncRegistration.getOsDestination().equals(appUri)) { 250 LogUtil.d("Expected destination to match with the supplied one!"); 251 return false; 252 } 253 254 if (appUri != null) { 255 builder.setAppDestinations(Collections.singletonList(getBaseUri(appUri))); 256 } 257 258 boolean shouldMatchAtLeastOneWebDestination = 259 asyncRegistration.isWebRequest() && asyncRegistration.getWebDestination() != null; 260 boolean matchedOneWebDestination = false; 261 262 if (!json.isNull(SourceHeaderContract.WEB_DESTINATION)) { 263 Set<Uri> destinationSet = new HashSet<>(); 264 JSONArray jsonDestinations; 265 Object obj = json.get(SourceHeaderContract.WEB_DESTINATION); 266 if (obj instanceof String) { 267 jsonDestinations = new JSONArray(); 268 jsonDestinations.put(json.getString(SourceHeaderContract.WEB_DESTINATION)); 269 } else { 270 jsonDestinations = json.getJSONArray(SourceHeaderContract.WEB_DESTINATION); 271 } 272 if (jsonDestinations.length() > MAX_DISTINCT_WEB_DESTINATIONS_IN_SOURCE_REGISTRATION) { 273 LogUtil.d("Source registration exceeded the number of allowed destinations."); 274 return false; 275 } 276 for (int i = 0; i < jsonDestinations.length(); i++) { 277 Uri destination = Uri.parse(jsonDestinations.getString(i)); 278 if (shouldMatchAtLeastOneWebDestination 279 && asyncRegistration.getWebDestination().equals(destination)) { 280 matchedOneWebDestination = true; 281 } 282 Optional<Uri> topPrivateDomainAndScheme = 283 Web.topPrivateDomainAndScheme(destination); 284 if (topPrivateDomainAndScheme.isEmpty()) { 285 LogUtil.d("Unable to extract top private domain and scheme from web " 286 + "destination."); 287 return false; 288 } else { 289 destinationSet.add(topPrivateDomainAndScheme.get()); 290 } 291 } 292 List<Uri> destinationList = new ArrayList<>(destinationSet); 293 builder.setWebDestinations(destinationList); 294 } 295 296 if (mFlags.getMeasurementEnableCoarseEventReportDestinations() 297 && !json.isNull(SourceHeaderContract.COARSE_EVENT_REPORT_DESTINATIONS)) { 298 builder.setCoarseEventReportDestinations( 299 json.getBoolean(SourceHeaderContract.COARSE_EVENT_REPORT_DESTINATIONS)); 300 } 301 302 if (shouldMatchAtLeastOneWebDestination && !matchedOneWebDestination) { 303 LogUtil.d("Expected at least one web_destination to match with the supplied one!"); 304 return false; 305 } 306 307 return true; 308 } 309 310 /** Parse a {@code Source}, given response headers, adding the {@code Source} to a given list */ 311 @VisibleForTesting parseSource( AsyncRegistration asyncRegistration, String enrollmentId, Map<String, List<String>> headers, AsyncFetchStatus asyncFetchStatus)312 public Optional<Source> parseSource( 313 AsyncRegistration asyncRegistration, 314 String enrollmentId, 315 Map<String, List<String>> headers, 316 AsyncFetchStatus asyncFetchStatus) { 317 boolean arDebugPermission = asyncRegistration.getDebugKeyAllowed(); 318 LogUtil.d("Source ArDebug permission enabled %b", arDebugPermission); 319 Source.Builder builder = new Source.Builder(); 320 builder.setRegistrationId(asyncRegistration.getRegistrationId()); 321 builder.setPublisher(getBaseUri(asyncRegistration.getTopOrigin())); 322 builder.setEnrollmentId(enrollmentId); 323 builder.setRegistrant(asyncRegistration.getRegistrant()); 324 builder.setSourceType(asyncRegistration.getSourceType()); 325 builder.setAttributionMode(Source.AttributionMode.TRUTHFULLY); 326 builder.setEventTime(asyncRegistration.getRequestTime()); 327 builder.setAdIdPermission(asyncRegistration.hasAdIdPermission()); 328 builder.setArDebugPermission(arDebugPermission); 329 builder.setPublisherType( 330 asyncRegistration.isWebRequest() ? EventSurfaceType.WEB : EventSurfaceType.APP); 331 Optional<Uri> registrationUriOrigin = 332 Web.originAndScheme(asyncRegistration.getRegistrationUri()); 333 if (!registrationUriOrigin.isPresent()) { 334 LogUtil.d( 335 "AsyncSourceFetcher: " 336 + "Invalid or empty registration uri - " 337 + asyncRegistration.getRegistrationUri()); 338 return Optional.empty(); 339 } 340 builder.setRegistrationOrigin(registrationUriOrigin.get()); 341 342 builder.setPlatformAdId( 343 FetcherUtil.getEncryptedPlatformAdIdIfPresent(asyncRegistration, enrollmentId)); 344 345 List<String> field = 346 headers.get(SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE); 347 if (field == null || field.size() != 1) { 348 LogUtil.d( 349 "AsyncSourceFetcher: " 350 + "Invalid Attribution-Reporting-Register-Source header."); 351 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.HEADER_ERROR); 352 return Optional.empty(); 353 } 354 try { 355 JSONObject json = new JSONObject(field.get(0)); 356 boolean isValid = 357 parseCommonSourceParams(json, asyncRegistration, builder, enrollmentId); 358 if (!isValid) { 359 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.VALIDATION_ERROR); 360 return Optional.empty(); 361 } 362 if (!json.isNull(SourceHeaderContract.AGGREGATION_KEYS)) { 363 if (!areValidAggregationKeys( 364 json.getJSONObject(SourceHeaderContract.AGGREGATION_KEYS))) { 365 asyncFetchStatus.setEntityStatus( 366 AsyncFetchStatus.EntityStatus.VALIDATION_ERROR); 367 return Optional.empty(); 368 } 369 builder.setAggregateSource(json.getString(SourceHeaderContract.AGGREGATION_KEYS)); 370 } 371 if (mFlags.getMeasurementEnableXNA() 372 && !json.isNull(SourceHeaderContract.SHARED_AGGREGATION_KEYS)) { 373 // Parsed as JSONArray for validation 374 JSONArray sharedAggregationKeys = 375 json.getJSONArray(SourceHeaderContract.SHARED_AGGREGATION_KEYS); 376 builder.setSharedAggregationKeys(sharedAggregationKeys.toString()); 377 } 378 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.SUCCESS); 379 return Optional.of(builder.build()); 380 } catch (JSONException | NumberFormatException e) { 381 LogUtil.d(e, "AsyncSourceFetcher: Invalid JSON"); 382 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.PARSING_ERROR); 383 return Optional.empty(); 384 } 385 } 386 hasRequiredParams(JSONObject json)387 private static boolean hasRequiredParams(JSONObject json) { 388 return !json.isNull(SourceHeaderContract.DESTINATION) 389 || !json.isNull(SourceHeaderContract.WEB_DESTINATION); 390 } 391 392 /** Provided a testing hook. */ 393 @NonNull 394 @VisibleForTesting openUrl(@onNull URL url)395 public URLConnection openUrl(@NonNull URL url) throws IOException { 396 return mNetworkConnection.setup(url); 397 } 398 399 /** 400 * Fetch a source type registration. 401 * 402 * @param asyncRegistration a {@link AsyncRegistration}, a request the record. 403 * @param asyncFetchStatus a {@link AsyncFetchStatus}, stores Ad Tech server status. 404 */ fetchSource( AsyncRegistration asyncRegistration, AsyncFetchStatus asyncFetchStatus, AsyncRedirect asyncRedirect)405 public Optional<Source> fetchSource( 406 AsyncRegistration asyncRegistration, 407 AsyncFetchStatus asyncFetchStatus, 408 AsyncRedirect asyncRedirect) { 409 HttpURLConnection urlConnection = null; 410 Map<String, List<String>> headers; 411 // TODO(b/276825561): Fix code duplication between fetchSource & fetchTrigger request flow 412 try { 413 urlConnection = 414 (HttpURLConnection) 415 openUrl(new URL(asyncRegistration.getRegistrationUri().toString())); 416 urlConnection.setRequestMethod("POST"); 417 urlConnection.setRequestProperty( 418 SourceRequestContract.SOURCE_INFO, 419 asyncRegistration.getSourceType().toString()); 420 urlConnection.setInstanceFollowRedirects(false); 421 headers = urlConnection.getHeaderFields(); 422 asyncFetchStatus.setResponseSize(FetcherUtil.calculateHeadersCharactersLength(headers)); 423 int responseCode = urlConnection.getResponseCode(); 424 LogUtil.d("Response code = " + responseCode); 425 if (!FetcherUtil.isRedirect(responseCode) && !FetcherUtil.isSuccess(responseCode)) { 426 asyncFetchStatus.setResponseStatus( 427 AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE); 428 return Optional.empty(); 429 } 430 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); 431 } catch (MalformedURLException e) { 432 LogUtil.d(e, "Malformed registration target URL"); 433 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.INVALID_URL); 434 return Optional.empty(); 435 } catch (IOException e) { 436 LogUtil.e(e, "Failed to get registration response"); 437 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR); 438 return Optional.empty(); 439 } finally { 440 if (urlConnection != null) { 441 urlConnection.disconnect(); 442 } 443 } 444 445 if (asyncRegistration.shouldProcessRedirects()) { 446 FetcherUtil.parseRedirects(headers).forEach(asyncRedirect::addToRedirects); 447 } 448 449 if (!isSourceHeaderPresent(headers)) { 450 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.HEADER_MISSING); 451 return Optional.empty(); 452 } 453 454 Optional<String> enrollmentId = 455 mFlags.isDisableMeasurementEnrollmentCheck() 456 ? Optional.of(Enrollment.FAKE_ENROLLMENT) 457 : Enrollment.getValidEnrollmentId( 458 asyncRegistration.getRegistrationUri(), 459 asyncRegistration.getRegistrant().getAuthority(), 460 mEnrollmentDao, 461 mContext, 462 mFlags); 463 if (enrollmentId.isEmpty()) { 464 LogUtil.d( 465 "fetchSource: Valid enrollment id not found. Registration URI: %s", 466 asyncRegistration.getRegistrationUri()); 467 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.INVALID_ENROLLMENT); 468 return Optional.empty(); 469 } 470 471 Optional<Source> parsedSource = 472 parseSource(asyncRegistration, enrollmentId.get(), headers, asyncFetchStatus); 473 return parsedSource; 474 } 475 isSourceHeaderPresent(Map<String, List<String>> headers)476 private boolean isSourceHeaderPresent(Map<String, List<String>> headers) { 477 return headers.containsKey( 478 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE); 479 } 480 areValidAggregationKeys(JSONObject aggregationKeys)481 private boolean areValidAggregationKeys(JSONObject aggregationKeys) { 482 if (aggregationKeys.length() > MAX_AGGREGATE_KEYS_PER_REGISTRATION) { 483 LogUtil.d( 484 "Aggregation-keys have more entries than permitted. %s", 485 aggregationKeys.length()); 486 return false; 487 } 488 for (String id : aggregationKeys.keySet()) { 489 if (!FetcherUtil.isValidAggregateKeyId(id)) { 490 LogUtil.d("SourceFetcher: aggregation key ID is invalid. %s", id); 491 return false; 492 } 493 String keyPiece = aggregationKeys.optString(id); 494 if (!FetcherUtil.isValidAggregateKeyPiece(keyPiece)) { 495 LogUtil.d("SourceFetcher: aggregation key-piece is invalid. %s", keyPiece); 496 return false; 497 } 498 } 499 return true; 500 } 501 roundSecondsToWholeDays(long seconds)502 private static long roundSecondsToWholeDays(long seconds) { 503 long remainder = seconds % ONE_DAY_IN_SECONDS; 504 boolean roundUp = remainder >= ONE_DAY_IN_SECONDS / 2L; 505 return seconds - remainder + (roundUp ? ONE_DAY_IN_SECONDS : 0); 506 } 507 508 private interface SourceHeaderContract { 509 String HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE = 510 "Attribution-Reporting-Register-Source"; 511 String SOURCE_EVENT_ID = "source_event_id"; 512 String DEBUG_KEY = "debug_key"; 513 String DESTINATION = "destination"; 514 String EXPIRY = "expiry"; 515 String EVENT_REPORT_WINDOW = "event_report_window"; 516 String AGGREGATABLE_REPORT_WINDOW = "aggregatable_report_window"; 517 String PRIORITY = "priority"; 518 String INSTALL_ATTRIBUTION_WINDOW_KEY = "install_attribution_window"; 519 String POST_INSTALL_EXCLUSIVITY_WINDOW_KEY = "post_install_exclusivity_window"; 520 String FILTER_DATA = "filter_data"; 521 String WEB_DESTINATION = "web_destination"; 522 String AGGREGATION_KEYS = "aggregation_keys"; 523 String SHARED_AGGREGATION_KEYS = "shared_aggregation_keys"; 524 String DEBUG_REPORTING = "debug_reporting"; 525 String DEBUG_JOIN_KEY = "debug_join_key"; 526 String DEBUG_AD_ID = "debug_ad_id"; 527 String COARSE_EVENT_REPORT_DESTINATIONS = "coarse_event_report_destinations"; 528 } 529 530 private interface SourceRequestContract { 531 String SOURCE_INFO = "Attribution-Reporting-Source-Info"; 532 } 533 } 534