• 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.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