• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 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