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