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 22 import com.android.adservices.service.Flags; 23 import com.android.adservices.service.measurement.PrivacyParams; 24 import com.android.adservices.service.measurement.ReportSpec; 25 import com.android.adservices.service.measurement.Source; 26 import com.android.adservices.service.measurement.reporting.EventReportWindowCalcDelegate; 27 import com.android.adservices.service.measurement.util.UnsignedLong; 28 29 import com.google.common.annotations.VisibleForTesting; 30 import com.google.common.collect.ImmutableList; 31 32 import java.math.BigDecimal; 33 import java.math.RoundingMode; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.Optional; 38 import java.util.Random; 39 import java.util.stream.Collectors; 40 41 /** Generates noised reports for the provided source. */ 42 public class SourceNoiseHandler { 43 private static final int PROBABILITY_DECIMAL_POINTS_LIMIT = 7; 44 45 private final Flags mFlags; 46 private final EventReportWindowCalcDelegate mEventReportWindowCalcDelegate; 47 SourceNoiseHandler(@onNull Flags flags)48 public SourceNoiseHandler(@NonNull Flags flags) { 49 mFlags = flags; 50 mEventReportWindowCalcDelegate = new EventReportWindowCalcDelegate(flags); 51 } 52 53 @VisibleForTesting SourceNoiseHandler( @onNull Flags flags, @NonNull EventReportWindowCalcDelegate eventReportWindowCalcDelegate)54 SourceNoiseHandler( 55 @NonNull Flags flags, 56 @NonNull EventReportWindowCalcDelegate eventReportWindowCalcDelegate) { 57 mFlags = flags; 58 mEventReportWindowCalcDelegate = eventReportWindowCalcDelegate; 59 } 60 61 /** Multiplier is 1, when only one destination needs to be considered. */ 62 public static final int SINGLE_DESTINATION_IMPRESSION_NOISE_MULTIPLIER = 1; 63 64 /** 65 * Double-folds the number of states in order to allocate half to app destination and half to 66 * web destination for fake reports generation. 67 */ 68 public static final int DUAL_DESTINATION_IMPRESSION_NOISE_MULTIPLIER = 2; 69 70 /** 71 * Assign attribution mode based on random rate and generate fake reports if needed. Should only 72 * be called for a new Source. 73 * 74 * @return fake reports to be stored in the datastore. 75 */ assignAttributionModeAndGenerateFakeReports( @onNull Source source)76 public List<Source.FakeReport> assignAttributionModeAndGenerateFakeReports( 77 @NonNull Source source) { 78 Random rand = new Random(); 79 double value = rand.nextDouble(); 80 if (value > getRandomAttributionProbability(source)) { 81 source.setAttributionMode(Source.AttributionMode.TRUTHFULLY); 82 return Collections.emptyList(); 83 } 84 85 List<Source.FakeReport> fakeReports; 86 ReportSpec flexEventReportSpec = source.getFlexEventReportSpec(); 87 if (flexEventReportSpec == null) { 88 if (isVtcDualDestinationModeWithPostInstallEnabled(source)) { 89 // Source is 'EVENT' type, both app and web destination are set and install 90 // exclusivity 91 // window is provided. Pick one of the static reporting states randomly. 92 fakeReports = generateVtcDualDestinationPostInstallFakeReports(source); 93 } else { 94 // There will at least be one (app or web) destination available 95 ImpressionNoiseParams noiseParams = getImpressionNoiseParams(source); 96 fakeReports = 97 ImpressionNoiseUtil.selectRandomStateAndGenerateReportConfigs( 98 noiseParams, rand) 99 .stream() 100 .map( 101 reportConfig -> 102 new Source.FakeReport( 103 new UnsignedLong( 104 Long.valueOf(reportConfig[0])), 105 mEventReportWindowCalcDelegate 106 .getReportingTimeForNoising( 107 source, 108 reportConfig[1], 109 isInstallDetectionEnabled( 110 source)), 111 resolveFakeReportDestinations( 112 source, reportConfig[2]))) 113 .collect(Collectors.toList()); 114 } 115 } else { 116 int destinationTypeMultiplier = getDestinationTypeMultiplier(source); 117 List<int[]> fakeReportConfigs = 118 ImpressionNoiseUtil.selectFlexEventReportRandomStateAndGenerateReportConfigs( 119 flexEventReportSpec, destinationTypeMultiplier, rand); 120 fakeReports = 121 fakeReportConfigs.stream() 122 .map( 123 reportConfig -> 124 new Source.FakeReport( 125 new UnsignedLong( 126 Long.valueOf( 127 flexEventReportSpec 128 .getTriggerDataValue( 129 reportConfig[ 130 0]))), 131 mEventReportWindowCalcDelegate 132 .getReportingTimeForNoisingFlexEventAPI( 133 reportConfig[1], 134 reportConfig[0], 135 flexEventReportSpec), 136 resolveFakeReportDestinations( 137 source, reportConfig[2]))) 138 .collect(Collectors.toList()); 139 } 140 @Source.AttributionMode 141 int attributionMode = 142 fakeReports.isEmpty() 143 ? Source.AttributionMode.NEVER 144 : Source.AttributionMode.FALSELY; 145 source.setAttributionMode(attributionMode); 146 return fakeReports; 147 } 148 149 /** @return Probability of selecting random state for attribution */ getRandomAttributionProbability(@onNull Source source)150 public double getRandomAttributionProbability(@NonNull Source source) { 151 if (mFlags.getMeasurementEnableConfigurableEventReportingWindows() 152 || mFlags.getMeasurementEnableVtcConfigurableMaxEventReports()) { 153 return calculateNoiseDynamically(source); 154 } 155 156 // Both destinations are set and install attribution is supported 157 if (!shouldReportCoarseDestinations(source) 158 && source.hasWebDestinations() 159 && isInstallDetectionEnabled(source)) { 160 return source.getSourceType() == Source.SourceType.EVENT 161 ? PrivacyParams.INSTALL_ATTR_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY 162 : PrivacyParams.INSTALL_ATTR_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY; 163 } 164 165 // Both destinations are set but install attribution isn't supported 166 if (!shouldReportCoarseDestinations(source) 167 && source.hasAppDestinations() 168 && source.hasWebDestinations()) { 169 return source.getSourceType() == Source.SourceType.EVENT 170 ? PrivacyParams.DUAL_DESTINATION_EVENT_NOISE_PROBABILITY 171 : PrivacyParams.DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY; 172 } 173 174 // App destination is set and install attribution is supported 175 if (isInstallDetectionEnabled(source)) { 176 return source.getSourceType() == Source.SourceType.EVENT 177 ? PrivacyParams.INSTALL_ATTR_EVENT_NOISE_PROBABILITY 178 : PrivacyParams.INSTALL_ATTR_NAVIGATION_NOISE_PROBABILITY; 179 } 180 181 // One of the destinations is available without install attribution support 182 return source.getSourceType() == Source.SourceType.EVENT 183 ? PrivacyParams.EVENT_NOISE_PROBABILITY 184 : PrivacyParams.NAVIGATION_NOISE_PROBABILITY; 185 } 186 calculateNoiseDynamically(Source source)187 private double calculateNoiseDynamically(Source source) { 188 int triggerDataCardinality = source.getTriggerDataCardinality(); 189 int reportingWindowCountForNoising = 190 mEventReportWindowCalcDelegate.getReportingWindowCountForNoising( 191 source, isInstallDetectionEnabled(source)); 192 int maxReportCount = 193 mEventReportWindowCalcDelegate.getMaxReportCount( 194 source, isInstallDetectionEnabled(source)); 195 int destinationMultiplier = getDestinationTypeMultiplier(source); 196 int numberOfStates = 197 Combinatorics.getNumberOfStarsAndBarsSequences( 198 /*numStars=*/ maxReportCount, 199 /*numBars=*/ triggerDataCardinality 200 * reportingWindowCountForNoising 201 * destinationMultiplier); 202 double absoluteProbability = Combinatorics.getFlipProbability(numberOfStates); 203 return BigDecimal.valueOf(absoluteProbability) 204 .setScale(PROBABILITY_DECIMAL_POINTS_LIMIT, RoundingMode.HALF_UP) 205 .doubleValue(); 206 } 207 isVtcDualDestinationModeWithPostInstallEnabled(Source source)208 private boolean isVtcDualDestinationModeWithPostInstallEnabled(Source source) { 209 return !shouldReportCoarseDestinations(source) 210 && source.getSourceType() == Source.SourceType.EVENT 211 && source.hasWebDestinations() 212 && isInstallDetectionEnabled(source); 213 } 214 215 /** 216 * Get the destination type multiplier, 217 * 218 * @return number of the destination type 219 */ getDestinationTypeMultiplier(Source source)220 private int getDestinationTypeMultiplier(Source source) { 221 return !shouldReportCoarseDestinations(source) 222 && source.hasAppDestinations() 223 && source.hasWebDestinations() 224 ? DUAL_DESTINATION_IMPRESSION_NOISE_MULTIPLIER 225 : SINGLE_DESTINATION_IMPRESSION_NOISE_MULTIPLIER; 226 } 227 228 /** 229 * Either both app and web destinations can be available or one of them will be available. When 230 * both destinations are available, we double the number of states at noise generation to be 231 * able to randomly choose one of them for fake report creation. We don't add the multiplier 232 * when only one of them is available. In that case, choose the one that's non-null. 233 * 234 * @param destinationIdentifier destination identifier, can be 0 (app) or 1 (web) 235 * @return app or web destination {@link Uri} 236 */ resolveFakeReportDestinations(Source source, int destinationIdentifier)237 private List<Uri> resolveFakeReportDestinations(Source source, int destinationIdentifier) { 238 if (shouldReportCoarseDestinations(source)) { 239 ImmutableList.Builder<Uri> destinations = new ImmutableList.Builder<>(); 240 Optional.ofNullable(source.getAppDestinations()).ifPresent(destinations::addAll); 241 Optional.ofNullable(source.getWebDestinations()).ifPresent(destinations::addAll); 242 return destinations.build(); 243 } 244 245 if (source.hasAppDestinations() && source.hasWebDestinations()) { 246 return destinationIdentifier % DUAL_DESTINATION_IMPRESSION_NOISE_MULTIPLIER == 0 247 ? source.getAppDestinations() 248 : source.getWebDestinations(); 249 } 250 251 return source.hasAppDestinations() 252 ? source.getAppDestinations() 253 : source.getWebDestinations(); 254 } 255 isInstallDetectionEnabled(@onNull Source source)256 private boolean isInstallDetectionEnabled(@NonNull Source source) { 257 return source.getInstallCooldownWindow() > 0 && source.hasAppDestinations(); 258 } 259 shouldReportCoarseDestinations(Source source)260 private boolean shouldReportCoarseDestinations(Source source) { 261 return mFlags.getMeasurementEnableCoarseEventReportDestinations() 262 && source.getCoarseEventReportDestinations(); 263 } 264 generateVtcDualDestinationPostInstallFakeReports( Source source)265 private List<Source.FakeReport> generateVtcDualDestinationPostInstallFakeReports( 266 Source source) { 267 int[][][] fakeReportsConfig = 268 ImpressionNoiseUtil.DUAL_DESTINATION_POST_INSTALL_FAKE_REPORT_CONFIG; 269 int randomIndex = new Random().nextInt(fakeReportsConfig.length); 270 int[][] reportsConfig = fakeReportsConfig[randomIndex]; 271 return Arrays.stream(reportsConfig) 272 .map( 273 reportConfig -> 274 new Source.FakeReport( 275 new UnsignedLong(Long.valueOf(reportConfig[0])), 276 mEventReportWindowCalcDelegate.getReportingTimeForNoising( 277 source, 278 /* window index */ reportConfig[1], 279 isInstallDetectionEnabled(source)), 280 resolveFakeReportDestinations(source, reportConfig[2]))) 281 .collect(Collectors.toList()); 282 } 283 284 @VisibleForTesting getImpressionNoiseParams(Source source)285 ImpressionNoiseParams getImpressionNoiseParams(Source source) { 286 int destinationTypeMultiplier = getDestinationTypeMultiplier(source); 287 return new ImpressionNoiseParams( 288 mEventReportWindowCalcDelegate.getMaxReportCount( 289 source, isInstallDetectionEnabled(source)), 290 source.getTriggerDataCardinality(), 291 mEventReportWindowCalcDelegate.getReportingWindowCountForNoising( 292 source, isInstallDetectionEnabled(source)), 293 destinationTypeMultiplier); 294 } 295 } 296