blob: 1411c2ee157a9190fd8d8cbac91ebce7184f34dd [file] [log] [blame]
/*
* Copyright (C) 2023 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;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Pair;
import com.android.adservices.LoggerFactory;
import com.android.adservices.service.measurement.noising.Combinatorics;
import com.android.adservices.service.measurement.util.UnsignedLong;
import com.android.internal.annotations.VisibleForTesting;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* A class wrapper for the trigger specification from the input argument during source registration
*/
public class TriggerSpecs {
private final TriggerSpec[] mTriggerSpecs;
private int mMaxEventLevelReports;
private final PrivacyComputationParams mPrivacyParams;
private final Map<UnsignedLong, Integer> mTriggerDataToTriggerSpecIndexMap = new HashMap<>();
// Reference to a list that is a property of the Source object.
private List<AttributedTrigger> mAttributedTriggersRef;
/** The JSON keys for flexible event report API input */
public interface FlexEventReportJsonKeys {
String VALUE = "value";
String PRIORITY = "priority";
String TRIGGER_TIME = "trigger_time";
String TRIGGER_DATA = "trigger_data";
String FLIP_PROBABILITY = "flip_probability";
String END_TIMES = "end_times";
String START_TIME = "start_time";
String SUMMARY_WINDOW_OPERATOR = "summary_window_operator";
String EVENT_REPORT_WINDOWS = "event_report_windows";
String SUMMARY_BUCKETS = "summary_buckets";
}
public TriggerSpecs(
String triggerSpecsString,
String maxEventLevelReports,
Source source,
String privacyParametersString)
throws JSONException {
this(
triggerSpecsString,
Integer.parseInt(maxEventLevelReports),
source,
privacyParametersString);
}
/**
* This constructor is called during the attribution process. Current trigger status will be
* read and process to determine the outcome of incoming trigger.
*
* @param triggerSpecsString input trigger specs from ad tech
* @param maxEventLevelReports max event level reports from ad tech
* @param source the source associated with this trigger specification
* @param privacyParametersString computed privacy parameters
* @throws JSONException JSON exception
*/
public TriggerSpecs(
String triggerSpecsString,
int maxEventLevelReports,
@Nullable Source source,
String privacyParametersString)
throws JSONException {
if (triggerSpecsString == null || triggerSpecsString.isEmpty()) {
throw new JSONException("the source is not registered as flexible event report API");
}
JSONArray triggerSpecs = new JSONArray(triggerSpecsString);
mTriggerSpecs = new TriggerSpec[triggerSpecs.length()];
for (int i = 0; i < triggerSpecs.length(); i++) {
mTriggerSpecs[i] = new TriggerSpec.Builder(triggerSpecs.getJSONObject(i)).build();
for (UnsignedLong triggerData : mTriggerSpecs[i].getTriggerData()) {
mTriggerDataToTriggerSpecIndexMap.put(triggerData, i);
}
}
mMaxEventLevelReports = maxEventLevelReports;
if (source != null) {
mAttributedTriggersRef = source.getAttributedTriggers();
}
mPrivacyParams = new PrivacyComputationParams(privacyParametersString);
}
/**
* This constructor is called during the source registration process.
*
* @param triggerSpecs trigger specs from ad tech
* @param maxEventLevelReports max event level reports from ad tech
* @param source the {@code Source} associated with this trigger specification
*/
public TriggerSpecs(@NonNull TriggerSpec[] triggerSpecs, int maxEventLevelReports,
Source source) {
mTriggerSpecs = triggerSpecs;
mMaxEventLevelReports = maxEventLevelReports;
mPrivacyParams = new PrivacyComputationParams();
if (source != null) {
mAttributedTriggersRef = source.getAttributedTriggers();
}
for (int i = 0; i < triggerSpecs.length; i++) {
for (UnsignedLong triggerData : triggerSpecs[i].getTriggerData()) {
mTriggerDataToTriggerSpecIndexMap.put(triggerData, i);
}
}
}
/**
* @return the information gain
*/
public double getInformationGain() {
return mPrivacyParams.getInformationGain();
}
/** @return the probability to use fake report */
public double getFlipProbability() {
return mPrivacyParams.getFlipProbability();
}
/**
* Get the parameters for the privacy computation. 1st element: total report cap, an array with
* 1 element is used to store the integer; 2nd element: number of windows per trigger data type;
* 3rd element: number of report cap per trigger data type.
*
* @return the parameters to computer number of states and fake report
*/
public int[][] getPrivacyParamsForComputation() {
int[][] params = new int[3][];
params[0] = new int[] {mMaxEventLevelReports};
params[1] = mPrivacyParams.getPerTypeNumWindowList();
params[2] = mPrivacyParams.getPerTypeCapList();
return params;
}
/**
* getter method for mTriggerSpecs
*
* @return the array of TriggerSpec
*/
public TriggerSpec[] getTriggerSpecs() {
return mTriggerSpecs;
}
/**
* @return Max number of reports)
*/
public int getMaxReports() {
return mMaxEventLevelReports;
}
/**
* Get the trigger datum given a trigger datum index. In the flexible event API, the trigger
* data are distributed uniquely among the trigger spec objects.
*
* @param triggerDataIndex The index of the triggerData
* @return the trigger data
*/
public UnsignedLong getTriggerDataFromIndex(int triggerDataIndex) {
for (TriggerSpec triggerSpec : mTriggerSpecs) {
int prevTriggerDataIndex = triggerDataIndex;
triggerDataIndex -= triggerSpec.getTriggerData().size();
if (triggerDataIndex < 0) {
return triggerSpec.getTriggerData().get(prevTriggerDataIndex);
}
}
// will not reach here
return null;
}
/**
* @param index the index of the summary bucket
* @param summaryBuckets the summary bucket
* @return return single summary bucket of the index
*/
public static Pair<Long, Long> getSummaryBucketFromIndex(
int index, @NonNull List<Long> summaryBuckets) {
return new Pair<>(
summaryBuckets.get(index),
index < summaryBuckets.size() - 1
? summaryBuckets.get(index + 1) - 1
: Integer.MAX_VALUE - 1);
}
/**
* @param triggerData the trigger data
* @return the summary bucket configured for the trigger data
*/
public List<Long> getSummaryBucketsForTriggerData(UnsignedLong triggerData) {
int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData);
return mTriggerSpecs[index].getSummaryBuckets();
}
/**
* @param triggerData the trigger data
* @return the summary operator type configured for the trigger data
*/
public TriggerSpec.SummaryOperatorType getSummaryOperatorType(UnsignedLong triggerData) {
int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData);
return mTriggerSpecs[index].getSummaryWindowOperator();
}
/**
* @param triggerData the trigger data
* @return the event report windows start time configured for the trigger data
*/
public Long findReportingStartTimeForTriggerData(UnsignedLong triggerData) {
int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData);
return mTriggerSpecs[index].getEventReportWindowsStart();
}
/**
* @param triggerData the trigger data
* @return the event report window ends configured for the trigger data
*/
public List<Long> findReportingEndTimesForTriggerData(UnsignedLong triggerData) {
int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData);
return mTriggerSpecs[index].getEventReportWindowsEnd();
}
/**
* Prepares structures for flex attribution handling.
*
* @param sourceEventReports delivered and pending reports for the source
* @param triggerTime trigger time
* @param reportsToDelete a list that the method will populate with reports to delete
* @param triggerDataToBucketIndexMap a map that the method will populate with the current
* bucket index per trigger data after considering delivered reports
*/
public void prepareFlexAttribution(
List<EventReport> sourceEventReports,
long triggerTime,
List<EventReport> reportsToDelete,
Map<UnsignedLong, Integer> triggerDataToBucketIndexMap) {
// Completed reports represent an ordered sequence of summary buckets.
sourceEventReports.sort(
Comparator.comparing(EventReport::getTriggerData)
.thenComparing(Comparator.comparingLong(
eventReport -> eventReport.getTriggerSummaryBucket().first)));
// Iterate over completed reports and store for each attributed trigger its contribution.
// Also record the list of pending reports to delete and recreate an updated sequence for.
for (EventReport eventReport : sourceEventReports) {
// Delete pending reports since we may have different ones based on new trigger priority
// ordering.
if (eventReport.getReportTime() >= triggerTime) {
reportsToDelete.add(eventReport);
continue;
}
UnsignedLong triggerData = eventReport.getTriggerData();
// Event reports are sorted by summary bucket so this event report must be either for
// the first or the next bucket. The index for the map is one higher, corresponding to
// the current bucket we'll start with for attribution.
triggerDataToBucketIndexMap.merge(triggerData, 1, (oldValue, value) -> oldValue + 1);
List<Long> buckets = getSummaryBucketsForTriggerData(triggerData);
int bucketIndex = triggerDataToBucketIndexMap.get(triggerData) - 1;
long prevBucket = bucketIndex == 0 ? 0L : buckets.get(bucketIndex - 1);
long bucketSize = buckets.get(bucketIndex) - prevBucket;
for (AttributedTrigger attributedTrigger : mAttributedTriggersRef) {
bucketSize -= restoreTriggerContributionAndGetBucketDelta(
attributedTrigger, eventReport, bucketSize);
// We've covered the triggers that contributed to this report so we can exit the
// iteration.
if (bucketSize == 0L) {
break;
}
}
}
}
private long restoreTriggerContributionAndGetBucketDelta(
AttributedTrigger attributedTrigger, EventReport eventReport, long bucketSize) {
// Skip this trigger since if it did not contribute to completed reports or if trigger data
// do not match.
if (attributedTrigger.getTriggerTime() >= eventReport.getReportTime()
|| !Objects.equals(attributedTrigger.getTriggerData(),
eventReport.getTriggerData())) {
return 0L;
}
// Value sum operator.
if (getSummaryOperatorType(eventReport.getTriggerData())
== TriggerSpec.SummaryOperatorType.VALUE_SUM) {
// The trigger can cover the full bucket size of the completed report.
if (attributedTrigger.remainingValue() >= bucketSize) {
attributedTrigger.addContribution(bucketSize);
return bucketSize;
// The trigger only covers some of the report's bucket.
} else {
long diff = attributedTrigger.remainingValue();
attributedTrigger.addContribution(diff);
return diff;
}
// Count operator for a trigger that we haven't counted yet.
} else if (attributedTrigger.getContribution() == 0L) {
attributedTrigger.addContribution(1L);
return 1L;
}
return 0L;
}
private int[] computePerTypeNumWindowList() {
List<Integer> list = new ArrayList<>();
for (TriggerSpec triggerSpec : mTriggerSpecs) {
for (UnsignedLong ignored : triggerSpec.getTriggerData()) {
list.add(triggerSpec.getEventReportWindowsEnd().size());
}
}
return list.stream().mapToInt(Integer::intValue).toArray();
}
private int[] computePerTypeCapList() {
List<Integer> list = new ArrayList<>();
for (TriggerSpec triggerSpec : mTriggerSpecs) {
for (UnsignedLong ignored : triggerSpec.getTriggerData()) {
list.add(triggerSpec.getSummaryBuckets().size());
}
}
return list.stream().mapToInt(Integer::intValue).toArray();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TriggerSpecs)) {
return false;
}
TriggerSpecs t = (TriggerSpecs) obj;
return mMaxEventLevelReports == t.mMaxEventLevelReports
&& Objects.equals(mAttributedTriggersRef, t.mAttributedTriggersRef)
&& Arrays.equals(mTriggerSpecs, t.mTriggerSpecs);
}
@Override
public int hashCode() {
return Objects.hash(
Arrays.hashCode(mTriggerSpecs),
mMaxEventLevelReports,
mAttributedTriggersRef);
}
/**
* Encode the privacy reporting parameters to JSON
*
* @return json object encode this class
*/
public String encodeToJson() {
return encodeToJson(mTriggerSpecs);
}
/**
* Encodes provided {@link TriggerSpec} into {@link JSONArray} string.
*
* @param triggerSpecs triggerSpec array to be encoded
* @return JSON encoded String
*/
public static String encodeToJson(TriggerSpec[] triggerSpecs) {
try {
JSONObject[] triggerSpecsArray = new JSONObject[triggerSpecs.length];
for (int i = 0; i < triggerSpecs.length; i++) {
triggerSpecsArray[i] = triggerSpecs[i].encodeJSON();
}
return new JSONArray(triggerSpecsArray).toString();
} catch (JSONException e) {
LoggerFactory.getMeasurementLogger()
.e("TriggerSpecs::encodeToJson is unable to encode TriggerSpecs");
return null;
}
}
/**
* Encode the result of privacy parameters computed based on input parameters to JSON
*
* @return String encoded the privacy parameters
*/
public String encodePrivacyParametersToJSONString() {
JSONObject json = new JSONObject();
try {
json.put(
FlexEventReportJsonKeys.FLIP_PROBABILITY,
mPrivacyParams.mFlipProbability);
} catch (JSONException e) {
LoggerFactory.getMeasurementLogger()
.e(
"TriggerSpecs::encodePrivacyParametersToJSONString is unable to encode"
+ " PrivacyParams to JSON");
return null;
}
return json.toString();
}
/**
* @param triggerData the triggerData to be checked
* @return whether the triggerData is registered
*/
public boolean containsTriggerData(UnsignedLong triggerData) {
return mTriggerDataToTriggerSpecIndexMap.containsKey(triggerData);
}
@VisibleForTesting
public List<AttributedTrigger> getAttributedTriggers() {
return mAttributedTriggersRef;
}
private class PrivacyComputationParams {
private final int[] mPerTypeNumWindowList;
private final int[] mPerTypeCapList;
private final long mNumStates;
private final double mFlipProbability;
private final double mInformationGain;
PrivacyComputationParams() {
mPerTypeNumWindowList = computePerTypeNumWindowList();
mPerTypeCapList = computePerTypeCapList();
// compute number of state and other privacy parameters
mNumStates =
Combinatorics.getNumStatesFlexApi(
mMaxEventLevelReports, mPerTypeNumWindowList, mPerTypeCapList);
mFlipProbability = Combinatorics.getFlipProbability(mNumStates);
mInformationGain = Combinatorics.getInformationGain(mNumStates, mFlipProbability);
}
PrivacyComputationParams(String inputLine) throws JSONException {
JSONObject json = new JSONObject(inputLine);
mFlipProbability =
json.getDouble(FlexEventReportJsonKeys.FLIP_PROBABILITY);
mPerTypeNumWindowList = null;
mPerTypeCapList = null;
mNumStates = -1;
mInformationGain = -1.0;
}
private double getFlipProbability() {
return mFlipProbability;
}
private double getInformationGain() {
return mInformationGain;
}
private int[] getPerTypeNumWindowList() {
return mPerTypeNumWindowList;
}
private int[] getPerTypeCapList() {
return mPerTypeCapList;
}
}
}