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