blob: ba7101def5fceb4cca50a99275d4ae34182f0c63 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.adservices.service.measurement.attribution;
import static com.android.adservices.service.measurement.PrivacyParams.AGGREGATE_MAX_REPORT_DELAY;
import static com.android.adservices.service.measurement.PrivacyParams.AGGREGATE_MIN_REPORT_DELAY;
import android.annotation.NonNull;
import android.net.Uri;
import android.util.Pair;
import com.android.adservices.LogUtil;
import com.android.adservices.data.measurement.DatastoreException;
import com.android.adservices.data.measurement.DatastoreManager;
import com.android.adservices.data.measurement.IMeasurementDao;
import com.android.adservices.service.Flags;
import com.android.adservices.service.FlagsFactory;
import com.android.adservices.service.measurement.Attribution;
import com.android.adservices.service.measurement.AttributionConfig;
import com.android.adservices.service.measurement.EventReport;
import com.android.adservices.service.measurement.EventSurfaceType;
import com.android.adservices.service.measurement.EventTrigger;
import com.android.adservices.service.measurement.FilterMap;
import com.android.adservices.service.measurement.PrivacyParams;
import com.android.adservices.service.measurement.Source;
import com.android.adservices.service.measurement.SystemHealthParams;
import com.android.adservices.service.measurement.Trigger;
import com.android.adservices.service.measurement.aggregation.AggregatableAttributionSource;
import com.android.adservices.service.measurement.aggregation.AggregatableAttributionTrigger;
import com.android.adservices.service.measurement.aggregation.AggregateAttributionData;
import com.android.adservices.service.measurement.aggregation.AggregateDeduplicationKey;
import com.android.adservices.service.measurement.aggregation.AggregateHistogramContribution;
import com.android.adservices.service.measurement.aggregation.AggregatePayloadGenerator;
import com.android.adservices.service.measurement.aggregation.AggregateReport;
import com.android.adservices.service.measurement.util.BaseUriExtractor;
import com.android.adservices.service.measurement.util.DebugKey;
import com.android.adservices.service.measurement.util.Filter;
import com.android.adservices.service.measurement.util.UnsignedLong;
import com.android.adservices.service.measurement.util.Web;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
class AttributionJobHandler {
private static final String API_VERSION = "0.1";
private final DatastoreManager mDatastoreManager;
private final Flags mFlags;
AttributionJobHandler(DatastoreManager datastoreManager) {
mDatastoreManager = datastoreManager;
mFlags = FlagsFactory.getFlags();
}
AttributionJobHandler(DatastoreManager datastoreManager, Flags flags) {
mDatastoreManager = datastoreManager;
mFlags = flags;
}
/**
* Perform attribution by finding relevant {@link Source} and generates {@link EventReport}.
*
* @return false if there are datastore failures or pending {@link Trigger} left, true otherwise
*/
synchronized boolean performPendingAttributions() {
Optional<List<String>> pendingTriggersOpt = mDatastoreManager
.runInTransactionWithResult(IMeasurementDao::getPendingTriggerIds);
if (!pendingTriggersOpt.isPresent()) {
// Failure during trigger retrieval
// Reschedule for retry
return false;
}
List<String> pendingTriggers = pendingTriggersOpt.get();
for (int i = 0; i < pendingTriggers.size()
&& i < SystemHealthParams.MAX_ATTRIBUTIONS_PER_INVOCATION; i++) {
boolean success = performAttribution(pendingTriggers.get(i));
if (!success) {
// Failure during trigger attribution
// Reschedule for retry
return false;
}
}
// Reschedule if there are unprocessed pending triggers.
return SystemHealthParams.MAX_ATTRIBUTIONS_PER_INVOCATION >= pendingTriggers.size();
}
/**
* Perform attribution for {@code triggerId}.
*
* @param triggerId datastore id of the {@link Trigger}
* @return success
*/
private boolean performAttribution(String triggerId) {
return mDatastoreManager.runInTransaction(
measurementDao -> {
Trigger trigger = measurementDao.getTrigger(triggerId);
if (trigger.getStatus() != Trigger.Status.PENDING) {
return;
}
Optional<Source> sourceOpt = selectSourceToAttribute(trigger, measurementDao);
if (sourceOpt.isEmpty()) {
ignoreTrigger(trigger, measurementDao);
return;
}
Source source = sourceOpt.get();
if (!doTopLevelFiltersMatch(source, trigger)) {
ignoreTrigger(trigger, measurementDao);
return;
}
if (!hasAttributionQuota(source, trigger, measurementDao)
|| !isEnrollmentWithinPrivacyBounds(source, trigger, measurementDao)) {
ignoreTrigger(trigger, measurementDao);
return;
}
boolean aggregateReportGenerated =
maybeGenerateAggregateReport(source, trigger, measurementDao);
boolean eventReportGenerated =
maybeGenerateEventReport(source, trigger, measurementDao);
if (eventReportGenerated || aggregateReportGenerated) {
attributeTriggerAndInsertAttribution(trigger, source, measurementDao);
} else {
ignoreTrigger(trigger, measurementDao);
}
});
}
private boolean maybeGenerateAggregateReport(Source source, Trigger trigger,
IMeasurementDao measurementDao) throws DatastoreException {
if (trigger.getTriggerTime() > source.getAggregatableReportWindow()) {
return false;
}
int numReports =
measurementDao.getNumAggregateReportsPerDestination(
trigger.getAttributionDestination(), trigger.getDestinationType());
if (numReports >= SystemHealthParams.getMaxAggregateReportsPerDestination()) {
LogUtil.d(
String.format(
Locale.ENGLISH,
"Aggregate reports for destination %1$s exceeds system health limit of"
+ " %2$d.",
trigger.getAttributionDestination(),
SystemHealthParams.getMaxAggregateReportsPerDestination()));
return false;
}
try {
Optional<AggregateDeduplicationKey> aggregateDeduplicationKey =
maybeGetAggregateDeduplicationKey(source, trigger);
if (aggregateDeduplicationKey.isPresent()
&& source.getAggregateReportDedupKeys()
.contains(aggregateDeduplicationKey.get().getDeduplicationKey())) {
return false;
}
Optional<List<AggregateHistogramContribution>> contributions =
AggregatePayloadGenerator.generateAttributionReport(source, trigger);
if (!contributions.isPresent()) {
return false;
}
OptionalInt newAggregateContributions =
validateAndGetUpdatedAggregateContributions(contributions.get(), source);
if (!newAggregateContributions.isPresent()) {
LogUtil.d(
"Aggregate contributions exceeded bound. Source ID: %s ; "
+ "Trigger ID: %s ",
source.getId(), trigger.getId());
return false;
}
source.setAggregateContributions(newAggregateContributions.getAsInt());
long randomTime =
(long)
((Math.random()
* (AGGREGATE_MAX_REPORT_DELAY
- AGGREGATE_MIN_REPORT_DELAY))
+ AGGREGATE_MIN_REPORT_DELAY);
Pair<UnsignedLong, UnsignedLong> debugKeyPair = DebugKey.getDebugKeys(source, trigger);
UnsignedLong sourceDebugKey = debugKeyPair.first;
UnsignedLong triggerDebugKey = debugKeyPair.second;
int debugReportStatus = AggregateReport.DebugReportStatus.NONE;
if (sourceDebugKey != null || triggerDebugKey != null) {
debugReportStatus = AggregateReport.DebugReportStatus.PENDING;
}
AggregateReport aggregateReport =
new AggregateReport.Builder()
// TODO: b/254855494 unused field, incorrect value; cleanup
.setPublisher(source.getRegistrant())
.setAttributionDestination(trigger.getAttributionDestinationBaseUri())
.setSourceRegistrationTime(roundDownToDay(source.getEventTime()))
.setScheduledReportTime(trigger.getTriggerTime() + randomTime)
.setEnrollmentId(trigger.getEnrollmentId())
.setDebugCleartextPayload(
AggregateReport.generateDebugPayload(contributions.get()))
.setAggregateAttributionData(
new AggregateAttributionData.Builder()
.setContributions(contributions.get())
.build())
.setStatus(AggregateReport.Status.PENDING)
.setDebugReportStatus(debugReportStatus)
.setApiVersion(API_VERSION)
.setSourceDebugKey(sourceDebugKey)
.setTriggerDebugKey(triggerDebugKey)
.setSourceId(source.getId())
.setTriggerId(trigger.getId())
.build();
finalizeAggregateReportCreation(
source, aggregateDeduplicationKey, aggregateReport, measurementDao);
// TODO (b/230618328): read from DB and upload unencrypted aggregate report.
return true;
} catch (JSONException e) {
LogUtil.e(e, "JSONException when parse aggregate fields in AttributionJobHandler.");
return false;
}
}
private Optional<Source> selectSourceToAttribute(
Trigger trigger, IMeasurementDao measurementDao) throws DatastoreException {
List<Source> matchingSources;
if (!mFlags.getMeasurementEnableXNA() || trigger.getAttributionConfig() == null) {
matchingSources = measurementDao.getMatchingActiveSources(trigger);
} else {
// XNA attribution is possible
Set<String> enrollmentIds = extractEnrollmentIds(trigger.getAttributionConfig());
List<Source> allSources =
measurementDao.fetchTriggerMatchingSourcesForXna(trigger, enrollmentIds);
List<Source> triggerEnrollmentMatchingSources = new ArrayList<>();
List<Source> otherEnrollmentBasedSources = new ArrayList<>();
for (Source source : allSources) {
if (Objects.equals(source.getEnrollmentId(), trigger.getEnrollmentId())) {
triggerEnrollmentMatchingSources.add(source);
} else {
otherEnrollmentBasedSources.add(source);
}
}
List<Source> derivedSources =
new XnaSourceCreator()
.generateDerivedSources(trigger, otherEnrollmentBasedSources);
matchingSources = new ArrayList<>();
matchingSources.addAll(triggerEnrollmentMatchingSources);
matchingSources.addAll(derivedSources);
}
if (matchingSources.isEmpty()) {
return Optional.empty();
}
// Sort based on isInstallAttributed, Priority and Event Time.
// Is a valid install-attributed source.
Function<Source, Boolean> installAttributionComparator =
(Source source) ->
source.isInstallAttributed()
&& isWithinInstallCooldownWindow(source, trigger);
matchingSources.sort(
Comparator.comparing(installAttributionComparator, Comparator.reverseOrder())
.thenComparing(Source::getPriority, Comparator.reverseOrder())
.thenComparing(Source::getEventTime, Comparator.reverseOrder()));
Source selectedSource = matchingSources.remove(0);
// Ignore all sources not selected for attribution
ignoreRemainingSources(measurementDao, matchingSources, trigger.getEnrollmentId());
return Optional.of(selectedSource);
}
private Set<String> extractEnrollmentIds(String attributionConfigsString) {
Set<String> enrollmentIds = new HashSet<>();
try {
JSONArray attributionConfigsJsonArray = new JSONArray(attributionConfigsString);
for (int i = 0; i < attributionConfigsJsonArray.length(); i++) {
JSONObject attributionConfigJson = attributionConfigsJsonArray.getJSONObject(i);
// It can't be null, has already been validated at fetcher
enrollmentIds.add(
attributionConfigJson.getString(
AttributionConfig.AttributionConfigContract.SOURCE_NETWORK));
}
} catch (JSONException e) {
LogUtil.d(e, "Failed to parse attribution configs.");
}
return enrollmentIds;
}
private Optional<AggregateDeduplicationKey> maybeGetAggregateDeduplicationKey(
Source source, Trigger trigger) {
try {
Optional<AggregateDeduplicationKey> dedupKey;
Optional<AggregatableAttributionSource> optionalAggregateAttributionSource =
source.getAggregatableAttributionSource();
Optional<AggregatableAttributionTrigger> optionalAggregateAttributionTrigger =
trigger.getAggregatableAttributionTrigger();
if (!optionalAggregateAttributionSource.isPresent()
|| !optionalAggregateAttributionTrigger.isPresent()) {
return Optional.empty();
}
AggregatableAttributionSource aggregateAttributionSource =
optionalAggregateAttributionSource.get();
AggregatableAttributionTrigger aggregateAttributionTrigger =
optionalAggregateAttributionTrigger.get();
dedupKey =
aggregateAttributionTrigger.maybeExtractDedupKey(
aggregateAttributionSource.getFilterMap());
return dedupKey;
} catch (JSONException e) {
LogUtil.e(
e,
"JSONException when parse aggregate dedup key fields in "
+ "AttributionJobHandler.");
return Optional.empty();
}
}
private void ignoreRemainingSources(
IMeasurementDao measurementDao,
List<Source> remainingSources,
String triggerEnrollmentId)
throws DatastoreException {
if (!remainingSources.isEmpty()) {
List<String> ignoredOriginalSourceIds = new ArrayList<>();
for (Source source : remainingSources) {
source.setStatus(Source.Status.IGNORED);
if (source.getParentId() == null) {
// Original source
ignoredOriginalSourceIds.add(source.getId());
} else {
// Derived source (XNA)
measurementDao.insertIgnoredSourceForEnrollment(
source.getParentId(), triggerEnrollmentId);
}
}
measurementDao.updateSourceStatus(ignoredOriginalSourceIds, Source.Status.IGNORED);
}
}
private boolean maybeGenerateEventReport(
Source source, Trigger trigger, IMeasurementDao measurementDao)
throws DatastoreException {
if (source.getParentId() != null) {
LogUtil.d("Event report generation skipped because it's a derived source.");
return false;
}
if (trigger.getTriggerTime() > source.getEventReportWindow()) {
return false;
}
int numReports =
measurementDao.getNumEventReportsPerDestination(
trigger.getAttributionDestination(), trigger.getDestinationType());
if (numReports >= SystemHealthParams.getMaxEventReportsPerDestination()) {
LogUtil.d(
String.format(
Locale.ENGLISH,
"Event reports for destination %1$s exceeds system health limit of"
+ " %2$d.",
trigger.getAttributionDestination(),
SystemHealthParams.getMaxEventReportsPerDestination()));
return false;
}
// Do not generate event reports for source which have attributionMode != Truthfully.
// TODO: Handle attribution rate limit consideration for non-truthful cases.
if (source.getAttributionMode() != Source.AttributionMode.TRUTHFULLY) {
return false;
}
Optional<EventTrigger> matchingEventTrigger =
findFirstMatchingEventTrigger(source, trigger);
if (!matchingEventTrigger.isPresent()) {
return false;
}
EventTrigger eventTrigger = matchingEventTrigger.get();
// Check if deduplication key clashes with existing reports.
if (eventTrigger.getDedupKey() != null
&& source.getEventReportDedupKeys().contains(eventTrigger.getDedupKey())) {
return false;
}
EventReport newEventReport =
new EventReport.Builder()
.populateFromSourceAndTrigger(source, trigger, eventTrigger)
.build();
if (!provisionEventReportQuota(source, trigger, newEventReport, measurementDao)) {
return false;
}
finalizeEventReportCreation(source, eventTrigger, newEventReport, measurementDao);
return true;
}
private boolean provisionEventReportQuota(Source source, Trigger trigger,
EventReport newEventReport, IMeasurementDao measurementDao)
throws DatastoreException {
List<EventReport> sourceEventReports = measurementDao.getSourceEventReports(source);
if (isWithinReportLimit(source, sourceEventReports.size(), trigger.getDestinationType())) {
return true;
}
List<EventReport> relevantEventReports =
sourceEventReports.stream()
.filter((r) -> r.getStatus() == EventReport.Status.PENDING)
.filter((r) -> r.getReportTime() == newEventReport.getReportTime())
.sorted(
Comparator.comparingLong(EventReport::getTriggerPriority)
.thenComparing(
EventReport::getTriggerTime,
Comparator.reverseOrder()))
.collect(Collectors.toList());
if (relevantEventReports.isEmpty()) {
return false;
}
EventReport lowestPriorityEventReport = relevantEventReports.get(0);
if (lowestPriorityEventReport.getTriggerPriority()
>= newEventReport.getTriggerPriority()) {
return false;
}
if (lowestPriorityEventReport.getTriggerDedupKey() != null) {
source.getEventReportDedupKeys().remove(lowestPriorityEventReport.getTriggerDedupKey());
}
measurementDao.deleteEventReport(lowestPriorityEventReport);
return true;
}
private void finalizeEventReportCreation(
Source source,
EventTrigger eventTrigger,
EventReport eventReport,
IMeasurementDao measurementDao)
throws DatastoreException {
if (eventTrigger.getDedupKey() != null) {
source.getEventReportDedupKeys().add(eventTrigger.getDedupKey());
}
measurementDao.updateSourceEventReportDedupKeys(source);
measurementDao.insertEventReport(eventReport);
}
private void finalizeAggregateReportCreation(
Source source,
Optional<AggregateDeduplicationKey> aggregateDeduplicationKey,
AggregateReport aggregateReport,
IMeasurementDao measurementDao)
throws DatastoreException {
if (aggregateDeduplicationKey.isPresent()) {
source.getAggregateReportDedupKeys()
.add(aggregateDeduplicationKey.get().getDeduplicationKey());
}
if (source.getParentId() == null) {
// Only update aggregate contributions for an original source, not for a derived
// source
measurementDao.updateSourceAggregateContributions(source);
measurementDao.updateSourceAggregateReportDedupKeys(source);
}
measurementDao.insertAggregateReport(aggregateReport);
}
private void attributeTriggerAndInsertAttribution(Trigger trigger, Source source,
IMeasurementDao measurementDao)
throws DatastoreException {
trigger.setStatus(Trigger.Status.ATTRIBUTED);
measurementDao.updateTriggerStatus(
Collections.singletonList(trigger.getId()), Trigger.Status.ATTRIBUTED);
measurementDao.insertAttribution(createAttribution(source, trigger));
}
private void ignoreTrigger(Trigger trigger, IMeasurementDao measurementDao)
throws DatastoreException {
trigger.setStatus(Trigger.Status.IGNORED);
measurementDao.updateTriggerStatus(
Collections.singletonList(trigger.getId()), Trigger.Status.IGNORED);
}
private boolean hasAttributionQuota(Source source, Trigger trigger,
IMeasurementDao measurementDao) throws DatastoreException {
long attributionCount =
measurementDao.getAttributionsPerRateLimitWindow(source, trigger);
return attributionCount < PrivacyParams.getMaxAttributionPerRateLimitWindow();
}
private boolean isWithinReportLimit(
Source source, int existingReportCount, @EventSurfaceType int destinationType) {
return source.getMaxReportCount(destinationType) > existingReportCount;
}
private boolean isWithinInstallCooldownWindow(Source source, Trigger trigger) {
return trigger.getTriggerTime()
< (source.getEventTime() + source.getInstallCooldownWindow());
}
/**
* The logic works as following - 1. If source OR trigger filters are empty, we call it a match
* since there is no restriction. 2. If source and trigger filters have no common keys, it's a
* match. 3. All common keys between source and trigger filters should have intersection between
* their list of values.
*
* @return true for a match, false otherwise
*/
private boolean doTopLevelFiltersMatch(@NonNull Source source, @NonNull Trigger trigger) {
try {
FilterMap sourceFilters = source.getFilterData();
List<FilterMap> triggerFilterSet = extractFilterSet(trigger.getFilters());
List<FilterMap> triggerNotFilterSet = extractFilterSet(trigger.getNotFilters());
return Filter.isFilterMatch(sourceFilters, triggerFilterSet, true)
&& Filter.isFilterMatch(sourceFilters, triggerNotFilterSet, false);
} catch (JSONException e) {
// If JSON is malformed, we shall consider as not matched.
LogUtil.e(e, "doTopLevelFiltersMatch: JSON parse failed.");
return false;
}
}
private Optional<EventTrigger> findFirstMatchingEventTrigger(Source source, Trigger trigger) {
try {
FilterMap sourceFiltersData = source.getFilterData();
List<EventTrigger> eventTriggers = trigger.parseEventTriggers();
return eventTriggers.stream()
.filter(
eventTrigger ->
doEventLevelFiltersMatch(sourceFiltersData, eventTrigger))
.findFirst();
} catch (JSONException e) {
// If JSON is malformed, we shall consider as not matched.
LogUtil.e(e, "Malformed JSON string.");
return Optional.empty();
}
}
private boolean doEventLevelFiltersMatch(
FilterMap sourceFiltersData, EventTrigger eventTrigger) {
if (eventTrigger.getFilterSet().isPresent()
&& !Filter.isFilterMatch(
sourceFiltersData, eventTrigger.getFilterSet().get(), true)) {
return false;
}
if (eventTrigger.getNotFilterSet().isPresent()
&& !Filter.isFilterMatch(
sourceFiltersData, eventTrigger.getNotFilterSet().get(), false)) {
return false;
}
return true;
}
private static List<FilterMap> extractFilterSet(String str) throws JSONException {
String json = (str == null || str.isEmpty()) ? "[]" : str;
List<FilterMap> filterSet = new ArrayList<>();
JSONArray filters = new JSONArray(json);
for (int i = 0; i < filters.length(); i++) {
FilterMap filterMap =
new FilterMap.Builder()
.buildFilterData(filters.getJSONObject(i))
.build();
filterSet.add(filterMap);
}
return filterSet;
}
private static OptionalInt validateAndGetUpdatedAggregateContributions(
List<AggregateHistogramContribution> contributions, Source source) {
int newAggregateContributions = source.getAggregateContributions();
for (AggregateHistogramContribution contribution : contributions) {
try {
newAggregateContributions =
Math.addExact(newAggregateContributions, contribution.getValue());
if (newAggregateContributions
> PrivacyParams.MAX_SUM_OF_AGGREGATE_VALUES_PER_SOURCE) {
return OptionalInt.empty();
}
} catch (ArithmeticException e) {
LogUtil.e(e, "Error adding aggregate contribution values.");
return OptionalInt.empty();
}
}
return OptionalInt.of(newAggregateContributions);
}
private static long roundDownToDay(long timestamp) {
return Math.floorDiv(timestamp, TimeUnit.DAYS.toMillis(1)) * TimeUnit.DAYS.toMillis(1);
}
private static boolean isEnrollmentWithinPrivacyBounds(Source source, Trigger trigger,
IMeasurementDao measurementDao) throws DatastoreException {
Optional<Pair<Uri, Uri>> publisherAndDestination =
getPublisherAndDestinationTopPrivateDomains(source, trigger);
if (publisherAndDestination.isPresent()) {
Integer count =
measurementDao.countDistinctEnrollmentsPerPublisherXDestinationInAttribution(
publisherAndDestination.get().first,
publisherAndDestination.get().second,
trigger.getEnrollmentId(),
trigger.getTriggerTime()
- PrivacyParams.RATE_LIMIT_WINDOW_MILLISECONDS,
trigger.getTriggerTime());
return count < PrivacyParams
.getMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution();
} else {
LogUtil.d("isEnrollmentWithinPrivacyBounds: getPublisherAndDestinationTopPrivateDomains"
+ " failed. %s %s", source.getPublisher(), trigger.getAttributionDestination());
return true;
}
}
private static Optional<Pair<Uri, Uri>> getPublisherAndDestinationTopPrivateDomains(
Source source, Trigger trigger) {
Uri attributionDestination = trigger.getAttributionDestination();
Optional<Uri> triggerDestinationTopPrivateDomain =
trigger.getDestinationType() == EventSurfaceType.APP
? Optional.of(BaseUriExtractor.getBaseUri(attributionDestination))
: Web.topPrivateDomainAndScheme(attributionDestination);
Uri publisher = source.getPublisher();
Optional<Uri> publisherTopPrivateDomain =
source.getPublisherType() == EventSurfaceType.APP
? Optional.of(publisher)
: Web.topPrivateDomainAndScheme(publisher);
if (!triggerDestinationTopPrivateDomain.isPresent()
|| !publisherTopPrivateDomain.isPresent()) {
return Optional.empty();
} else {
return Optional.of(Pair.create(
publisherTopPrivateDomain.get(),
triggerDestinationTopPrivateDomain.get()));
}
}
public Attribution createAttribution(@NonNull Source source, @NonNull Trigger trigger) {
Optional<Uri> publisherTopPrivateDomain =
getTopPrivateDomain(source.getPublisher(), source.getPublisherType());
Uri destination = trigger.getAttributionDestination();
Optional<Uri> destinationTopPrivateDomain =
getTopPrivateDomain(destination, trigger.getDestinationType());
if (!publisherTopPrivateDomain.isPresent()
|| !destinationTopPrivateDomain.isPresent()) {
throw new IllegalArgumentException(
String.format(
"insertAttributionRateLimit: "
+ "getSourceAndDestinationTopPrivateDomains"
+ " failed. Publisher: %s; Attribution destination: %s",
source.getPublisher(), destination));
}
return new Attribution.Builder()
.setSourceSite(publisherTopPrivateDomain.get().toString())
.setSourceOrigin(source.getPublisher().toString())
.setDestinationSite(destinationTopPrivateDomain.get().toString())
.setDestinationOrigin(BaseUriExtractor.getBaseUri(destination).toString())
.setEnrollmentId(trigger.getEnrollmentId())
.setTriggerTime(trigger.getTriggerTime())
.setRegistrant(trigger.getRegistrant().toString())
.setSourceId(source.getId())
.setTriggerId(trigger.getId())
.build();
}
private static Optional<Uri> getTopPrivateDomain(
Uri uri, @EventSurfaceType int eventSurfaceType) {
return eventSurfaceType == EventSurfaceType.APP
? Optional.of(BaseUriExtractor.getBaseUri(uri))
: Web.topPrivateDomainAndScheme(uri);
}
}