1 /*
2  * Copyright (C) 2023 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 
17 package com.android.adservices.service.measurement;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.util.Pair;
22 
23 import com.android.adservices.LoggerFactory;
24 import com.android.adservices.service.Flags;
25 import com.android.adservices.service.measurement.noising.Combinatorics;
26 import com.android.adservices.service.measurement.util.UnsignedLong;
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import org.json.JSONArray;
30 import org.json.JSONException;
31 import org.json.JSONObject;
32 
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Comparator;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Objects;
40 
41 /**
42  * A class wrapper for the trigger specification from the input argument during source registration
43  */
44 public class TriggerSpecs {
45     private final TriggerSpec[] mTriggerSpecs;
46     private int mMaxEventLevelReports;
47     private PrivacyComputationParams mPrivacyParams;
48     private final Map<UnsignedLong, Integer> mTriggerDataToTriggerSpecIndexMap = new HashMap<>();
49     // Reference to a list that is a property of the Source object.
50     private List<AttributedTrigger> mAttributedTriggersRef;
51 
52     // Trigger data magnitude is restricted to 32 bits.
53     public static final UnsignedLong MAX_TRIGGER_DATA_VALUE = new UnsignedLong((1L << 32) - 1L);
54     // Max bucket threshold is 32 bits.
55     public static final long MAX_BUCKET_THRESHOLD = (1L << 32) - 1L;
56 
57     /** The JSON keys for flexible event report API input */
58     public interface FlexEventReportJsonKeys {
59         String VALUE = "value";
60         String PRIORITY = "priority";
61         String TRIGGER_TIME = "trigger_time";
62         String TRIGGER_DATA = "trigger_data";
63         String FLIP_PROBABILITY = "flip_probability";
64         String END_TIMES = "end_times";
65         String START_TIME = "start_time";
66         String SUMMARY_WINDOW_OPERATOR = "summary_window_operator";
67         String EVENT_REPORT_WINDOWS = "event_report_windows";
68         String SUMMARY_BUCKETS = "summary_buckets";
69     }
70 
TriggerSpecs( String triggerSpecsString, String maxEventLevelReports, Source source, String privacyParametersString)71     public TriggerSpecs(
72             String triggerSpecsString,
73             String maxEventLevelReports,
74             Source source,
75             String privacyParametersString)
76             throws JSONException {
77         this(
78                 triggerSpecsString,
79                 Integer.parseInt(maxEventLevelReports),
80                 source,
81                 privacyParametersString);
82     }
83 
84     /**
85      * This constructor is called during the attribution process. Current trigger status will be
86      * read and process to determine the outcome of incoming trigger.
87      *
88      * @param triggerSpecsString input trigger specs from ad tech
89      * @param maxEventLevelReports max event level reports from ad tech
90      * @param source the source associated with this trigger specification
91      * @param privacyParametersString computed privacy parameters
92      * @throws JSONException JSON exception
93      */
TriggerSpecs( String triggerSpecsString, int maxEventLevelReports, @Nullable Source source, String privacyParametersString)94     public TriggerSpecs(
95             String triggerSpecsString,
96             int maxEventLevelReports,
97             @Nullable Source source,
98             String privacyParametersString)
99             throws JSONException {
100         if (triggerSpecsString == null || triggerSpecsString.isEmpty()) {
101             throw new JSONException("the source is not registered as flexible event report API");
102         }
103         JSONArray triggerSpecs = new JSONArray(triggerSpecsString);
104         mTriggerSpecs = new TriggerSpec[triggerSpecs.length()];
105         for (int i = 0; i < triggerSpecs.length(); i++) {
106             mTriggerSpecs[i] = new TriggerSpec.Builder(triggerSpecs.getJSONObject(i)).build();
107             for (UnsignedLong triggerData : mTriggerSpecs[i].getTriggerData()) {
108                 mTriggerDataToTriggerSpecIndexMap.put(triggerData, i);
109             }
110         }
111 
112         mMaxEventLevelReports = maxEventLevelReports;
113 
114         if (source != null) {
115             mAttributedTriggersRef = source.getAttributedTriggers();
116         }
117 
118         mPrivacyParams = new PrivacyComputationParams(privacyParametersString);
119     }
120 
121     /**
122      * This constructor is called during the source registration process.
123      *
124      * @param triggerSpecs trigger specs from ad tech
125      * @param maxEventLevelReports max event level reports from ad tech
126      * @param source the {@code Source} associated with this trigger specification
127      */
TriggerSpecs(@onNull TriggerSpec[] triggerSpecs, int maxEventLevelReports, Source source)128     public TriggerSpecs(@NonNull TriggerSpec[] triggerSpecs, int maxEventLevelReports,
129             Source source) {
130         mTriggerSpecs = triggerSpecs;
131         mMaxEventLevelReports = maxEventLevelReports;
132         if (source != null) {
133             mAttributedTriggersRef = source.getAttributedTriggers();
134         }
135         for (int i = 0; i < triggerSpecs.length; i++) {
136             for (UnsignedLong triggerData : triggerSpecs[i].getTriggerData()) {
137                 mTriggerDataToTriggerSpecIndexMap.put(triggerData, i);
138             }
139         }
140     }
141 
142     /**
143      * @return the information gain
144      */
getInformationGain(Source source, Flags flags)145     public double getInformationGain(Source source, Flags flags) {
146         if (mPrivacyParams == null) {
147             buildPrivacyParameters(source, flags);
148         }
149         return mPrivacyParams.getInformationGain();
150     }
151 
152     /** @return whether the trigger specs have a valid report state count */
hasValidReportStateCount(Source source, Flags flags)153     public boolean hasValidReportStateCount(Source source, Flags flags) {
154         if (mPrivacyParams == null) {
155             buildPrivacyParameters(source, flags);
156         }
157         return mPrivacyParams.hasValidReportStateCount();
158     }
159 
160     /**
161      * @return The number of report state counts for the trigger specifications, or 0 if invalid.
162      */
getNumStates(Source source, Flags flags)163     public long getNumStates(Source source, Flags flags) {
164         if (mPrivacyParams == null) {
165             buildPrivacyParameters(source, flags);
166         }
167         return mPrivacyParams.getNumStates();
168     }
169 
170     /** @return the probability to use fake report */
getFlipProbability(Source source, Flags flags)171     public double getFlipProbability(Source source, Flags flags) {
172         if (mPrivacyParams == null) {
173             buildPrivacyParameters(source, flags);
174         }
175         return mPrivacyParams.getFlipProbability();
176     }
177 
178     /**
179      * Get the parameters for the privacy computation. 1st element: total report cap, an array with
180      * 1 element is used to store the integer; 2nd element: number of windows per trigger data type;
181      * 3rd element: number of report cap per trigger data type.
182      *
183      * @return the parameters to computer number of states and fake report
184      */
getPrivacyParamsForComputation()185     public int[][] getPrivacyParamsForComputation() {
186         // TODO (b/313920181): build privacy params in case null.
187         int[][] params = new int[3][];
188         params[0] = new int[] {mMaxEventLevelReports};
189         params[1] = mPrivacyParams.getPerTypeNumWindowList();
190         params[2] = mPrivacyParams.getPerTypeCapList();
191         return params;
192     }
193 
194     /**
195      * getter method for mTriggerSpecs
196      *
197      * @return the array of TriggerSpec
198      */
getTriggerSpecs()199     public TriggerSpec[] getTriggerSpecs() {
200         return mTriggerSpecs;
201     }
202 
203     /**
204      * @return Max number of reports)
205      */
getMaxReports()206     public int getMaxReports() {
207         return mMaxEventLevelReports;
208     }
209 
210     /**
211      * Get the trigger datum given a trigger datum index. In the flexible event API, the trigger
212      * data are distributed uniquely among the trigger spec objects.
213      *
214      * @param triggerDataIndex The index of the triggerData
215      * @return the trigger data
216      */
getTriggerDataFromIndex(int triggerDataIndex)217     public UnsignedLong getTriggerDataFromIndex(int triggerDataIndex) {
218         for (TriggerSpec triggerSpec : mTriggerSpecs) {
219             int prevTriggerDataIndex = triggerDataIndex;
220             triggerDataIndex -= triggerSpec.getTriggerData().size();
221             if (triggerDataIndex < 0) {
222                 return triggerSpec.getTriggerData().get(prevTriggerDataIndex);
223             }
224         }
225         // will not reach here
226         return null;
227     }
228 
229     /**
230      * @param index the index of the summary bucket
231      * @param summaryBuckets the summary bucket
232      * @return return single summary bucket of the index
233      */
getSummaryBucketFromIndex( int index, @NonNull List<Long> summaryBuckets)234     public static Pair<Long, Long> getSummaryBucketFromIndex(
235             int index, @NonNull List<Long> summaryBuckets) {
236         return new Pair<>(
237                 summaryBuckets.get(index),
238                 index < summaryBuckets.size() - 1
239                         ? summaryBuckets.get(index + 1) - 1
240                         : MAX_BUCKET_THRESHOLD);
241     }
242 
243    /**
244      * @param triggerData the trigger data
245      * @return the summary bucket configured for the trigger data
246      */
getSummaryBucketsForTriggerData(UnsignedLong triggerData)247     public List<Long> getSummaryBucketsForTriggerData(UnsignedLong triggerData) {
248         int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData);
249         return mTriggerSpecs[index].getSummaryBuckets();
250     }
251 
252     /**
253      * @param triggerData the trigger data
254      * @return the summary operator type configured for the trigger data
255      */
getSummaryOperatorType(UnsignedLong triggerData)256     public TriggerSpec.SummaryOperatorType getSummaryOperatorType(UnsignedLong triggerData) {
257         int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData);
258         return mTriggerSpecs[index].getSummaryWindowOperator();
259     }
260 
261     /**
262      * @param triggerData the trigger data
263      * @return the event report windows start time configured for the trigger data
264      */
findReportingStartTimeForTriggerData(UnsignedLong triggerData)265     public Long findReportingStartTimeForTriggerData(UnsignedLong triggerData) {
266         int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData);
267         return mTriggerSpecs[index].getEventReportWindowsStart();
268     }
269 
270     /**
271      * @param triggerData the trigger data
272      * @return the event report window ends configured for the trigger data
273      */
findReportingEndTimesForTriggerData(UnsignedLong triggerData)274     public List<Long> findReportingEndTimesForTriggerData(UnsignedLong triggerData) {
275         int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData);
276         return mTriggerSpecs[index].getEventReportWindowsEnd();
277     }
278 
279     /**
280      * Prepares structures for flex attribution handling.
281      *
282      * @param sourceEventReports delivered and pending reports for the source
283      * @param triggerTime trigger time
284      * @param reportsToDelete a list that the method will populate with reports to delete
285      * @param triggerDataToBucketIndexMap a map that the method will populate with the current
286      * bucket index per trigger data after considering delivered reports
287      */
prepareFlexAttribution( List<EventReport> sourceEventReports, long triggerTime, List<EventReport> reportsToDelete, Map<UnsignedLong, Integer> triggerDataToBucketIndexMap)288     public void prepareFlexAttribution(
289             List<EventReport> sourceEventReports,
290             long triggerTime,
291             List<EventReport> reportsToDelete,
292             Map<UnsignedLong, Integer> triggerDataToBucketIndexMap) {
293         // Completed reports represent an ordered sequence of summary buckets.
294         sourceEventReports.sort(
295                 Comparator.comparing(EventReport::getTriggerData)
296                         .thenComparing(Comparator.comparingLong(
297                                 eventReport -> eventReport.getTriggerSummaryBucket().first)));
298 
299         // Iterate over completed reports and store for each attributed trigger its contribution.
300         // Also record the list of pending reports to delete and recreate an updated sequence for.
301         for (EventReport eventReport : sourceEventReports) {
302             // Delete pending reports since we may have different ones based on new trigger priority
303             // ordering.
304             if (eventReport.getReportTime() > triggerTime) {
305                 reportsToDelete.add(eventReport);
306                 continue;
307             }
308 
309             UnsignedLong triggerData = eventReport.getTriggerData();
310 
311             // Event reports are sorted by summary bucket so this event report must be either for
312             // the first or the next bucket. The index for the map is one higher, corresponding to
313             // the current bucket we'll start with for attribution.
314             triggerDataToBucketIndexMap.merge(triggerData, 1, (oldValue, value) -> oldValue + 1);
315 
316             List<Long> buckets = getSummaryBucketsForTriggerData(triggerData);
317             int bucketIndex = triggerDataToBucketIndexMap.get(triggerData) - 1;
318             long prevBucket = bucketIndex == 0 ? 0L : buckets.get(bucketIndex - 1);
319             long bucketSize = buckets.get(bucketIndex) - prevBucket;
320 
321             for (AttributedTrigger attributedTrigger : mAttributedTriggersRef) {
322                 bucketSize -= restoreTriggerContributionAndGetBucketDelta(
323                         attributedTrigger, eventReport, bucketSize);
324                 // We've covered the triggers that contributed to this report so we can exit the
325                 // iteration.
326                 if (bucketSize == 0L) {
327                     break;
328                 }
329             }
330         }
331     }
332 
restoreTriggerContributionAndGetBucketDelta( AttributedTrigger attributedTrigger, EventReport eventReport, long bucketSize)333     private long restoreTriggerContributionAndGetBucketDelta(
334             AttributedTrigger attributedTrigger, EventReport eventReport, long bucketSize) {
335         // Skip this trigger since if it did not contribute to completed reports or if trigger data
336         // do not match.
337         if (attributedTrigger.getTriggerTime() >= eventReport.getReportTime()
338                 || !Objects.equals(attributedTrigger.getTriggerData(),
339                         eventReport.getTriggerData())) {
340             return 0L;
341         }
342 
343         // Value sum operator.
344         if (getSummaryOperatorType(eventReport.getTriggerData())
345                 == TriggerSpec.SummaryOperatorType.VALUE_SUM) {
346             // The trigger can cover the full bucket size of the completed report.
347             if (attributedTrigger.remainingValue() >= bucketSize) {
348                 attributedTrigger.addContribution(bucketSize);
349                 return bucketSize;
350             // The trigger only covers some of the report's bucket.
351             } else {
352                 long diff = attributedTrigger.remainingValue();
353                 attributedTrigger.addContribution(diff);
354                 return diff;
355             }
356         // Count operator for a trigger that we haven't counted yet.
357         } else if (attributedTrigger.getContribution() == 0L) {
358             attributedTrigger.addContribution(1L);
359             return 1L;
360         }
361 
362         return 0L;
363     }
364 
buildPrivacyParameters(Source source, Flags flags)365     private void buildPrivacyParameters(Source source, Flags flags) {
366         mPrivacyParams = new PrivacyComputationParams(source, flags);
367     }
368 
computePerTypeNumWindowList()369     private int[] computePerTypeNumWindowList() {
370         List<Integer> list = new ArrayList<>();
371         for (TriggerSpec triggerSpec : mTriggerSpecs) {
372             for (UnsignedLong ignored : triggerSpec.getTriggerData()) {
373                 list.add(triggerSpec.getEventReportWindowsEnd().size());
374             }
375         }
376         return list.stream().mapToInt(Integer::intValue).toArray();
377     }
378 
computePerTypeCapList()379     private int[] computePerTypeCapList() {
380         List<Integer> list = new ArrayList<>();
381         for (TriggerSpec triggerSpec : mTriggerSpecs) {
382             for (UnsignedLong ignored : triggerSpec.getTriggerData()) {
383                 list.add(triggerSpec.getSummaryBuckets().size());
384             }
385         }
386         return list.stream().mapToInt(Integer::intValue).toArray();
387     }
388 
389     @Override
equals(Object obj)390     public boolean equals(Object obj) {
391         if (!(obj instanceof TriggerSpecs)) {
392             return false;
393         }
394         TriggerSpecs t = (TriggerSpecs) obj;
395 
396         return mMaxEventLevelReports == t.mMaxEventLevelReports
397                 && Objects.equals(mAttributedTriggersRef, t.mAttributedTriggersRef)
398                 && Arrays.equals(mTriggerSpecs, t.mTriggerSpecs);
399     }
400 
401     @Override
hashCode()402     public int hashCode() {
403         return Objects.hash(
404                 Arrays.hashCode(mTriggerSpecs),
405                 mMaxEventLevelReports,
406                 mAttributedTriggersRef);
407     }
408 
409     /**
410      * Encode the privacy reporting parameters to JSON
411      *
412      * @return json object encode this class
413      */
encodeToJson()414     public String encodeToJson() {
415         return encodeToJson(mTriggerSpecs);
416     }
417 
418     /**
419      * Encodes provided {@link TriggerSpec} into {@link JSONArray} string.
420      *
421      * @param triggerSpecs triggerSpec array to be encoded
422      * @return JSON encoded String
423      */
encodeToJson(TriggerSpec[] triggerSpecs)424     public static String encodeToJson(TriggerSpec[] triggerSpecs) {
425         try {
426             JSONObject[] triggerSpecsArray = new JSONObject[triggerSpecs.length];
427             for (int i = 0; i < triggerSpecs.length; i++) {
428                 triggerSpecsArray[i] = triggerSpecs[i].encodeJSON();
429             }
430             return new JSONArray(triggerSpecsArray).toString();
431         } catch (JSONException e) {
432             LoggerFactory.getMeasurementLogger()
433                     .e("TriggerSpecs::encodeToJson is unable to encode TriggerSpecs");
434             return null;
435         }
436     }
437 
438     /**
439      * Encode the result of privacy parameters computed based on input parameters to JSON
440      *
441      * @return String encoded the privacy parameters
442      */
encodePrivacyParametersToJsonString()443     public String encodePrivacyParametersToJsonString() {
444         JSONObject json = new JSONObject();
445         try {
446             json.put(
447                     FlexEventReportJsonKeys.FLIP_PROBABILITY,
448                     mPrivacyParams.mFlipProbability);
449         } catch (JSONException e) {
450             LoggerFactory.getMeasurementLogger()
451                     .e(
452                             "TriggerSpecs::encodePrivacyParametersToJsonString is unable to encode"
453                                     + " PrivacyParams to JSON");
454             return null;
455         }
456         return json.toString();
457     }
458 
459     /**
460      * @param triggerData the triggerData to be checked
461      * @return whether the triggerData is registered
462      */
containsTriggerData(UnsignedLong triggerData)463     public boolean containsTriggerData(UnsignedLong triggerData) {
464         return mTriggerDataToTriggerSpecIndexMap.containsKey(triggerData);
465     }
466 
467     /**
468      * @return the trigger data cardinality across all trigger specs
469      */
getTriggerDataCardinality()470     public int getTriggerDataCardinality() {
471         return mTriggerDataToTriggerSpecIndexMap.size();
472     }
473 
474     @VisibleForTesting
getAttributedTriggers()475     public List<AttributedTrigger> getAttributedTriggers() {
476         return mAttributedTriggersRef;
477     }
478 
479     private class PrivacyComputationParams {
480         private final int[] mPerTypeNumWindowList;
481         private final int[] mPerTypeCapList;
482         private long mNumStates;
483         private double mFlipProbability;
484         private double mInformationGain;
485 
PrivacyComputationParams(Source source, Flags flags)486         PrivacyComputationParams(Source source, Flags flags) {
487             mPerTypeNumWindowList = computePerTypeNumWindowList();
488             mPerTypeCapList = computePerTypeCapList();
489 
490             // Doubling the window cap for each trigger data type correlates with counting report
491             // states that treat having a web destination as different from an app destination.
492             int destinationMultiplier = source.getDestinationTypeMultiplier(flags);
493             int[] updatedPerTypeNumWindowList = new int[mPerTypeNumWindowList.length];
494             for (int i = 0; i < mPerTypeNumWindowList.length; i++) {
495                 updatedPerTypeNumWindowList[i] = mPerTypeNumWindowList[i] * destinationMultiplier;
496             }
497 
498             long reportStateCountLimit = flags.getMeasurementMaxReportStatesPerSourceRegistration();
499 
500             long numStates = Combinatorics.getNumStatesFlexApi(
501                     mMaxEventLevelReports,
502                     updatedPerTypeNumWindowList,
503                     mPerTypeCapList,
504                     reportStateCountLimit);
505 
506             if (numStates > reportStateCountLimit) {
507                 return;
508             }
509 
510             mNumStates = numStates;
511             mFlipProbability =
512                     Combinatorics.getFlipProbability(
513                             mNumStates, (double) flags.getMeasurementPrivacyEpsilon());
514             mInformationGain = Combinatorics.getInformationGain(mNumStates, mFlipProbability);
515         }
516 
PrivacyComputationParams(String inputLine)517         PrivacyComputationParams(String inputLine) throws JSONException {
518             JSONObject json = new JSONObject(inputLine);
519             mFlipProbability =
520                     json.getDouble(FlexEventReportJsonKeys.FLIP_PROBABILITY);
521             mPerTypeNumWindowList = null;
522             mPerTypeCapList = null;
523             mNumStates = -1;
524             mInformationGain = -1.0;
525         }
526 
hasValidReportStateCount()527         private boolean hasValidReportStateCount() {
528             return mNumStates != 0;
529         }
530 
getNumStates()531         private long getNumStates() {
532             return mNumStates;
533         }
534 
getFlipProbability()535         private double getFlipProbability() {
536             return mFlipProbability;
537         }
538 
getInformationGain()539         private double getInformationGain() {
540             return mInformationGain;
541         }
542 
getPerTypeNumWindowList()543         private int[] getPerTypeNumWindowList() {
544             return mPerTypeNumWindowList;
545         }
546 
getPerTypeCapList()547         private int[] getPerTypeCapList() {
548             return mPerTypeCapList;
549         }
550     }
551 }
552