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