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.noising;
18 
19 import android.annotation.NonNull;
20 import android.net.Uri;
21 import android.util.Pair;
22 
23 import com.android.adservices.service.Flags;
24 import com.android.adservices.service.measurement.Source;
25 import com.android.adservices.service.measurement.TriggerSpecs;
26 import com.android.adservices.service.measurement.reporting.EventReportWindowCalcDelegate;
27 import com.android.adservices.service.measurement.util.UnsignedLong;
28 import com.android.internal.annotations.VisibleForTesting;
29 
30 import com.google.common.collect.ImmutableList;
31 
32 import java.math.BigDecimal;
33 import java.math.RoundingMode;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Optional;
37 import java.util.concurrent.ThreadLocalRandom;
38 import java.util.stream.Collectors;
39 
40 /** Generates noised reports for the provided source. */
41 public class SourceNoiseHandler {
42     private static final int PROBABILITY_DECIMAL_POINTS_LIMIT = 7;
43 
44     private final Flags mFlags;
45     private final EventReportWindowCalcDelegate mEventReportWindowCalcDelegate;
46 
SourceNoiseHandler(@onNull Flags flags)47     public SourceNoiseHandler(@NonNull Flags flags) {
48         mFlags = flags;
49         mEventReportWindowCalcDelegate = new EventReportWindowCalcDelegate(flags);
50     }
51 
52     @VisibleForTesting
SourceNoiseHandler( @onNull Flags flags, @NonNull EventReportWindowCalcDelegate eventReportWindowCalcDelegate)53     SourceNoiseHandler(
54             @NonNull Flags flags,
55             @NonNull EventReportWindowCalcDelegate eventReportWindowCalcDelegate) {
56         mFlags = flags;
57         mEventReportWindowCalcDelegate = eventReportWindowCalcDelegate;
58     }
59 
60     /** Multiplier is 1, when only one destination needs to be considered. */
61     public static final int SINGLE_DESTINATION_IMPRESSION_NOISE_MULTIPLIER = 1;
62 
63     /**
64      * Double-folds the number of states in order to allocate half to app destination and half to
65      * web destination for fake reports generation.
66      */
67     public static final int DUAL_DESTINATION_IMPRESSION_NOISE_MULTIPLIER = 2;
68 
69     /**
70      * Assign attribution mode based on random rate and generate fake reports if needed. Should only
71      * be called for a new Source.
72      *
73      * @return fake reports to be stored in the datastore.
74      */
assignAttributionModeAndGenerateFakeReports( @onNull Source source)75     public List<Source.FakeReport> assignAttributionModeAndGenerateFakeReports(
76             @NonNull Source source) {
77         ThreadLocalRandom rand = ThreadLocalRandom.current();
78         double value = rand.nextDouble();
79         if (value >= getRandomizedSourceResponsePickRate(source)) {
80             source.setAttributionMode(Source.AttributionMode.TRUTHFULLY);
81             return null;
82         }
83 
84         List<Source.FakeReport> fakeReports = new ArrayList<>();
85         TriggerSpecs triggerSpecs = source.getTriggerSpecs();
86 
87         if (triggerSpecs == null) {
88             // There will at least be one (app or web) destination available
89             ImpressionNoiseParams noiseParams = getImpressionNoiseParams(source);
90             fakeReports =
91                     ImpressionNoiseUtil.selectRandomStateAndGenerateReportConfigs(noiseParams, rand)
92                             .stream()
93                             .map(
94                                     reportConfig -> {
95                                         long triggerTime = source.getEventTime();
96                                         long reportingTime =
97                                                 mEventReportWindowCalcDelegate
98                                                         .getReportingTimeForNoising(
99                                                                 source, reportConfig[1]);
100                                         if (mFlags.getMeasurementEnableAttributionScope()) {
101                                             Pair<Long, Long> reportingAndTriggerTime =
102                                                     mEventReportWindowCalcDelegate
103                                                             .getReportingAndTriggerTimeForNoising(
104                                                                     source, reportConfig[1]);
105                                             triggerTime = reportingAndTriggerTime.first;
106                                             reportingTime = reportingAndTriggerTime.second;
107                                         }
108                                         return new Source.FakeReport(
109                                                 new UnsignedLong(Long.valueOf(reportConfig[0])),
110                                                 reportingTime,
111                                                 triggerTime,
112                                                 resolveFakeReportDestinations(
113                                                         source, reportConfig[2]),
114                                                 /* triggerSummaryBucket = */ null);
115                                     })
116                             .collect(Collectors.toList());
117         } else {
118             int destinationTypeMultiplier = source.getDestinationTypeMultiplier(mFlags);
119             List<int[]> fakeReportConfigs =
120                     ImpressionNoiseUtil.selectFlexEventReportRandomStateAndGenerateReportConfigs(
121                             triggerSpecs, destinationTypeMultiplier, rand);
122 
123             // Group configurations by trigger data, ordered by window index.
124             fakeReportConfigs.sort((config1, config2) -> {
125                 UnsignedLong triggerData1 = triggerSpecs.getTriggerDataFromIndex(config1[0]);
126                 UnsignedLong triggerData2 = triggerSpecs.getTriggerDataFromIndex(config2[0]);
127 
128                 if (triggerData1.equals(triggerData2)) {
129                     return Integer.valueOf(config1[1]).compareTo(Integer.valueOf(config2[1]));
130                 }
131 
132                 return triggerData1.compareTo(triggerData2);
133             });
134 
135             int bucketIndex = -1;
136             UnsignedLong currentTriggerData = null;
137             List<Long> buckets = new ArrayList<>();
138 
139             for (int[] reportConfig : fakeReportConfigs) {
140                 UnsignedLong triggerData = triggerSpecs.getTriggerDataFromIndex(reportConfig[0]);
141 
142                 // A new group of trigger data ordered by report index.
143                 if (!triggerData.equals(currentTriggerData)) {
144                     buckets = triggerSpecs.getSummaryBucketsForTriggerData(triggerData);
145                     bucketIndex = 0;
146                     currentTriggerData = triggerData;
147                 // The same trigger data, the next report ordered by window index will have the next
148                 // trigger summary bucket.
149                 } else {
150                     bucketIndex += 1;
151                 }
152 
153                 Pair<Long, Long> triggerSummaryBucket =
154                         TriggerSpecs.getSummaryBucketFromIndex(bucketIndex, buckets);
155                 long triggerTime = source.getEventTime();
156                 long reportingTime =
157                         mEventReportWindowCalcDelegate
158                                 .getReportingTimeForNoisingFlexEventApi(
159                                         reportConfig[1],
160                                         reportConfig[0],
161                                         source);
162 
163                 if (mFlags.getMeasurementEnableAttributionScope()) {
164                     Pair<Long, Long> reportingAndTriggerTime =
165                             mEventReportWindowCalcDelegate
166                                     .getReportingAndTriggerTimeForNoisingFlexEventApi(
167                                             reportConfig[1],
168                                             reportConfig[0],
169                                             source);
170                     triggerTime = reportingAndTriggerTime.first;
171                     reportingTime = reportingAndTriggerTime.second;
172                 }
173 
174                 fakeReports.add(new Source.FakeReport(
175                         currentTriggerData,
176                         reportingTime,
177                         triggerTime,
178                         resolveFakeReportDestinations(
179                                 source, reportConfig[2]),
180                         triggerSummaryBucket));
181             }
182         }
183         @Source.AttributionMode
184         int attributionMode =
185                 fakeReports.isEmpty()
186                         ? Source.AttributionMode.NEVER
187                         : Source.AttributionMode.FALSELY;
188         source.setAttributionMode(attributionMode);
189         return fakeReports;
190     }
191 
192     @VisibleForTesting
getRandomizedSourceResponsePickRate(Source source)193     double getRandomizedSourceResponsePickRate(Source source) {
194         // Methods on Source and EventReportWindowCalcDelegate that calculate flip probability for
195         // the source rely on reporting windows and max reports that are obtained with consideration
196         // to install-state and its interaction with configurable report windows and configurable
197         // max reports.
198         return source.getFlipProbability(mFlags);
199     }
200 
201     /** @return Probability of selecting random state for attribution */
getRandomizedTriggerRate(@onNull Source source)202     public double getRandomizedTriggerRate(@NonNull Source source) {
203         return convertToDoubleAndLimitDecimal(getRandomizedSourceResponsePickRate(source));
204     }
205 
convertToDoubleAndLimitDecimal(double probability)206     private double convertToDoubleAndLimitDecimal(double probability) {
207         return BigDecimal.valueOf(probability)
208                 .setScale(PROBABILITY_DECIMAL_POINTS_LIMIT, RoundingMode.HALF_UP)
209                 .doubleValue();
210     }
211 
212     /**
213      * Either both app and web destinations can be available or one of them will be available. When
214      * both destinations are available, we double the number of states at noise generation to be
215      * able to randomly choose one of them for fake report creation. We don't add the multiplier
216      * when only one of them is available. In that case, choose the one that's non-null.
217      *
218      * @param destinationIdentifier destination identifier, can be 0 (app) or 1 (web)
219      * @return app or web destination {@link Uri}
220      */
resolveFakeReportDestinations(Source source, int destinationIdentifier)221     private List<Uri> resolveFakeReportDestinations(Source source, int destinationIdentifier) {
222         if (source.shouldReportCoarseDestinations(mFlags)) {
223             ImmutableList.Builder<Uri> destinations = new ImmutableList.Builder<>();
224             Optional.ofNullable(source.getAppDestinations()).ifPresent(destinations::addAll);
225             Optional.ofNullable(source.getWebDestinations()).ifPresent(destinations::addAll);
226             return destinations.build();
227         }
228 
229         if (source.hasAppDestinations() && source.hasWebDestinations()) {
230             return destinationIdentifier % DUAL_DESTINATION_IMPRESSION_NOISE_MULTIPLIER == 0
231                     ? source.getAppDestinations()
232                     : source.getWebDestinations();
233         }
234 
235         return source.hasAppDestinations()
236                 ? source.getAppDestinations()
237                 : source.getWebDestinations();
238     }
239 
240     @VisibleForTesting
getImpressionNoiseParams(Source source)241     ImpressionNoiseParams getImpressionNoiseParams(Source source) {
242         int destinationTypeMultiplier = source.getDestinationTypeMultiplier(mFlags);
243         return new ImpressionNoiseParams(
244                 mEventReportWindowCalcDelegate.getMaxReportCount(source),
245                 source.getTriggerDataCardinality(),
246                 mEventReportWindowCalcDelegate.getReportingWindowCountForNoising(source),
247                 destinationTypeMultiplier);
248     }
249 }
250