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.reporting; 18 19 import static com.android.adservices.service.measurement.PrivacyParams.EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS; 20 import static com.android.adservices.service.measurement.PrivacyParams.INSTALL_ATTR_EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS; 21 import static com.android.adservices.service.measurement.PrivacyParams.INSTALL_ATTR_NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS; 22 import static com.android.adservices.service.measurement.PrivacyParams.MAX_CONFIGURABLE_EVENT_REPORT_EARLY_REPORTING_WINDOWS; 23 import static com.android.adservices.service.measurement.PrivacyParams.NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS; 24 25 import android.annotation.NonNull; 26 import android.util.Pair; 27 28 import com.android.adservices.LoggerFactory; 29 import com.android.adservices.service.Flags; 30 import com.android.adservices.service.measurement.EventSurfaceType; 31 import com.android.adservices.service.measurement.PrivacyParams; 32 import com.android.adservices.service.measurement.Source; 33 import com.android.adservices.service.measurement.Trigger; 34 import com.android.adservices.service.measurement.TriggerSpec; 35 import com.android.adservices.service.measurement.TriggerSpecs; 36 import com.android.adservices.service.measurement.util.UnsignedLong; 37 38 import com.google.common.collect.ImmutableList; 39 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import java.util.List; 43 import java.util.concurrent.TimeUnit; 44 import java.util.stream.Collectors; 45 46 /** Does event report window related calculations, e.g. count, reporting time. */ 47 public class EventReportWindowCalcDelegate { 48 private static final String EARLY_REPORTING_WINDOWS_CONFIG_DELIMITER = ","; 49 50 private final Flags mFlags; 51 EventReportWindowCalcDelegate(@onNull Flags flags)52 public EventReportWindowCalcDelegate(@NonNull Flags flags) { 53 mFlags = flags; 54 } 55 56 /** 57 * Max reports count given the Source object. 58 * 59 * @param source the Source object 60 * @return maximum number of reports allowed 61 */ getMaxReportCount(Source source)62 public int getMaxReportCount(Source source) { 63 return getMaxReportCount(source, source.isInstallDetectionEnabled()); 64 } 65 66 /** 67 * Max reports count based on conversion destination type. 68 * 69 * @param source the Source object 70 * @param destinationType destination type 71 * @return maximum number of reports allowed 72 */ getMaxReportCount(@onNull Source source, @EventSurfaceType int destinationType)73 public int getMaxReportCount(@NonNull Source source, @EventSurfaceType int destinationType) { 74 return getMaxReportCount(source, isInstallCase(source, destinationType)); 75 } 76 getMaxReportCount(@onNull Source source, boolean isInstallCase)77 private int getMaxReportCount(@NonNull Source source, boolean isInstallCase) { 78 if (source.getMaxEventLevelReports() != null) { 79 return source.getMaxEventLevelReports(); 80 } 81 82 if (source.getSourceType() == Source.SourceType.EVENT) { 83 // Additional report essentially for first open + 1 post install conversion. If there 84 // is already more than 1 report allowed, no need to have that additional report. 85 if (isInstallCase && !source.hasWebDestinations() && isDefaultConfiguredVtc()) { 86 return PrivacyParams.INSTALL_ATTR_EVENT_SOURCE_MAX_REPORTS; 87 } 88 return mFlags.getMeasurementVtcConfigurableMaxEventReportsCount(); 89 } 90 91 return PrivacyParams.NAVIGATION_SOURCE_MAX_REPORTS; 92 } 93 94 /** 95 * Calculates the reporting time based on the {@link Trigger} time, {@link Source}'s expiry and 96 * trigger destination type. 97 * 98 * @return the reporting time 99 */ getReportingTime( @onNull Source source, long triggerTime, @EventSurfaceType int destinationType)100 public long getReportingTime( 101 @NonNull Source source, long triggerTime, @EventSurfaceType int destinationType) { 102 if (triggerTime < source.getEventTime()) { 103 return -1; 104 } 105 106 // Cases where source could have both web and app destinations, there if the trigger 107 // destination is an app, and it was installed, then installState should be considered true. 108 List<Pair<Long, Long>> reportingWindows = 109 getEffectiveReportingWindows(source, isInstallCase(source, destinationType)); 110 for (Pair<Long, Long> window : reportingWindows) { 111 if (isWithinWindow(triggerTime, window)) { 112 return window.second; 113 } 114 } 115 116 return -1; 117 } 118 isWithinWindow(long time, Pair<Long, Long> window)119 private boolean isWithinWindow(long time, Pair<Long, Long> window) { 120 return window.first <= time && time < window.second; 121 } 122 123 /** 124 * Enum shows trigger time and source window time relationship. It is used to generate different 125 * verbose debug reports. 126 */ 127 public enum MomentPlacement { 128 BEFORE, 129 AFTER, 130 WITHIN; 131 } 132 133 /** 134 * @param source source for which the window is calculated 135 * @param trigger trigger 136 * @param triggerData trigger data 137 * @return how trigger time falls in source windows 138 */ fallsWithinWindow( @onNull Source source, @NonNull Trigger trigger, @NonNull UnsignedLong triggerData)139 public MomentPlacement fallsWithinWindow( 140 @NonNull Source source, 141 @NonNull Trigger trigger, 142 @NonNull UnsignedLong triggerData) { 143 long triggerTime = trigger.getTriggerTime(); 144 145 // Non-flex source 146 if (source.getTriggerSpecs() == null) { 147 List<Pair<Long, Long>> reportingWindows = 148 getEffectiveReportingWindows( 149 source, isInstallCase(source, trigger.getDestinationType())); 150 if (triggerTime < reportingWindows.get(0).first) { 151 return MomentPlacement.BEFORE; 152 } 153 if (triggerTime >= reportingWindows.get(reportingWindows.size() - 1).second) { 154 return MomentPlacement.AFTER; 155 } 156 return MomentPlacement.WITHIN; 157 } 158 159 // Flex source 160 TriggerSpecs triggerSpecs = source.getTriggerSpecs(); 161 long sourceRegistrationTime = source.getEventTime(); 162 163 if (triggerTime 164 < triggerSpecs.findReportingStartTimeForTriggerData(triggerData) 165 + sourceRegistrationTime) { 166 return MomentPlacement.BEFORE; 167 } 168 169 List<Long> reportingWindows = triggerSpecs.findReportingEndTimesForTriggerData(triggerData); 170 for (Long window : reportingWindows) { 171 if (triggerTime < window + sourceRegistrationTime) { 172 return MomentPlacement.WITHIN; 173 } 174 } 175 return MomentPlacement.AFTER; 176 } 177 178 /** 179 * Return reporting time by index for noising based on the index 180 * 181 * @param windowIndex index of the reporting window for which 182 * @return reporting time in milliseconds 183 * @deprecated use {@link #getReportingAndTriggerTimeForNoising} instead. 184 */ 185 @Deprecated getReportingTimeForNoising(@onNull Source source, int windowIndex)186 public long getReportingTimeForNoising(@NonNull Source source, int windowIndex) { 187 List<Pair<Long, Long>> reportingWindows = getEffectiveReportingWindows( 188 source, source.isInstallDetectionEnabled()); 189 Pair<Long, Long> finalWindow = reportingWindows.get(reportingWindows.size() - 1); 190 // TODO: (b/288646239) remove this check, confirming noising indexing accuracy. 191 return windowIndex < reportingWindows.size() 192 ? reportingWindows.get(windowIndex).second 193 : finalWindow.second; 194 } 195 196 /** 197 * Return a pair of trigger and reporting time by index for noising based on the index 198 * 199 * @param windowIndex index of the reporting window for which 200 * @return a pair of reporting and trigger time in milliseconds 201 */ getReportingAndTriggerTimeForNoising( @onNull Source source, int windowIndex)202 public Pair<Long, Long> getReportingAndTriggerTimeForNoising( 203 @NonNull Source source, int windowIndex) { 204 List<Pair<Long, Long>> reportingWindows = 205 getEffectiveReportingWindows(source, source.isInstallDetectionEnabled()); 206 Pair<Long, Long> finalWindow = reportingWindows.get(reportingWindows.size() - 1); 207 // TODO: (b/288646239) remove this check, confirming noising indexing accuracy. 208 long triggerTime = 209 windowIndex < reportingWindows.size() 210 ? reportingWindows.get(windowIndex).first 211 : finalWindow.first; 212 long reportingTime = 213 windowIndex < reportingWindows.size() 214 ? reportingWindows.get(windowIndex).second 215 : finalWindow.second; 216 return Pair.create(triggerTime, reportingTime); 217 } 218 219 /** 220 * Returns effective, that is, the ones that occur before {@link 221 * Source#getEffectiveEventReportWindow()}, event reporting windows count for noising cases. 222 * 223 * @param source source for which the count is requested 224 */ 225 public int getReportingWindowCountForNoising(@NonNull Source source) { 226 return getEffectiveReportingWindows(source, source.isInstallDetectionEnabled()).size(); 227 } 228 229 /** 230 * Returns reporting time for noising with flex event API. 231 * 232 * @param windowIndex Window index corresponding to which the reporting time should be returned. 233 * @param triggerDataIndex Trigger data state index. 234 * @param source The source. 235 * @deprecated use {@link #getReportingAndTriggerTimeForNoisingFlexEventApi} instead. 236 */ 237 @Deprecated 238 public long getReportingTimeForNoisingFlexEventApi( 239 int windowIndex, int triggerDataIndex, @NonNull Source source) { 240 for (TriggerSpec triggerSpec : source.getTriggerSpecs().getTriggerSpecs()) { 241 triggerDataIndex -= triggerSpec.getTriggerData().size(); 242 if (triggerDataIndex < 0) { 243 return source.getEventTime() 244 + triggerSpec.getEventReportWindowsEnd().get(windowIndex); 245 } 246 } 247 return 0; 248 } 249 250 /** 251 * Returns a pair of trigger and reporting time for noising with flex event API. 252 * 253 * @param windowIndex window index corresponding to which the reporting time should be returned. 254 * @param triggerDataIndex trigger data state index. 255 */ 256 public Pair<Long, Long> getReportingAndTriggerTimeForNoisingFlexEventApi( 257 int windowIndex, int triggerDataIndex, @NonNull Source source) { 258 for (TriggerSpec triggerSpec : source.getTriggerSpecs().getTriggerSpecs()) { 259 triggerDataIndex -= triggerSpec.getTriggerData().size(); 260 if (triggerDataIndex < 0) { 261 long triggerTime = 262 source.getEventTime() 263 + (windowIndex == 0 264 ? triggerSpec.getEventReportWindowsStart() 265 : triggerSpec 266 .getEventReportWindowsEnd() 267 .get(windowIndex - 1)); 268 long reportingTime = 269 source.getEventTime() 270 + triggerSpec.getEventReportWindowsEnd().get(windowIndex); 271 return Pair.create(triggerTime, reportingTime); 272 } 273 } 274 return Pair.create(0L, 0L); 275 } 276 277 /** 278 * Calculates the reporting time based on the {@link Trigger} time for flexible event report API 279 * 280 * @param triggerSpecs the report specification to be processed 281 * @param sourceRegistrationTime source registration time 282 * @param triggerTime trigger time 283 * @param triggerData the trigger data 284 * @return the reporting time 285 */ 286 public long getFlexEventReportingTime( 287 TriggerSpecs triggerSpecs, 288 long sourceRegistrationTime, 289 long triggerTime, 290 UnsignedLong triggerData) { 291 if (triggerTime < sourceRegistrationTime) { 292 return -1L; 293 } 294 if (triggerTime 295 < triggerSpecs.findReportingStartTimeForTriggerData(triggerData) 296 + sourceRegistrationTime) { 297 return -1L; 298 } 299 300 List<Long> reportingWindows = triggerSpecs.findReportingEndTimesForTriggerData(triggerData); 301 for (Long window : reportingWindows) { 302 if (triggerTime < window + sourceRegistrationTime) { 303 return sourceRegistrationTime + window; 304 } 305 } 306 return -1L; 307 } 308 309 private static boolean isInstallCase(Source source, @EventSurfaceType int destinationType) { 310 return destinationType == EventSurfaceType.APP && source.isInstallAttributed(); 311 } 312 313 /** 314 * If the flag is enabled and the specified report windows are valid, picks from flag controlled 315 * configurable early reporting windows. Otherwise, falls back to the values provided in 316 * {@code getDefaultEarlyReportingWindowEnds}, which can have install-related custom behaviour. 317 * It curtails the windows that occur after {@link Source#getEffectiveEventReportWindow()} 318 * because they would effectively be unusable. 319 */ 320 private List<Pair<Long, Long>> getEffectiveReportingWindows(Source source, 321 boolean installState) { 322 // TODO(b/290221611) Remove early reporting windows from code, only use them for flags. 323 if (source.hasManualEventReportWindows()) { 324 return source.parsedProcessedEventReportWindows(); 325 } 326 List<Long> defaultEarlyWindowEnds = 327 getDefaultEarlyReportingWindowEnds( 328 source.getSourceType(), 329 installState && !source.hasWebDestinations()); 330 List<Long> earlyWindowEnds = 331 getConfiguredOrDefaultEarlyReportingWindowEnds( 332 source.getSourceType(), defaultEarlyWindowEnds); 333 // Add source event time to windows 334 earlyWindowEnds = 335 earlyWindowEnds.stream() 336 .map((x) -> source.getEventTime() + x) 337 .collect(Collectors.toList()); 338 339 List<Pair<Long, Long>> windowList = new ArrayList<>(); 340 long windowStart = source.getEventTime(); 341 Pair<Long, Long> finalWindow = 342 getFinalReportingWindow(source, earlyWindowEnds); 343 344 for (long windowEnd : earlyWindowEnds) { 345 // Start time of `finalWindow` is either source event time or one of 346 // `earlyWindowEnds` times; stop iterating if we see it, and add `finalWindow`. 347 if (windowStart == finalWindow.first) { 348 break; 349 } 350 windowList.add(Pair.create(windowStart, windowEnd)); 351 windowStart = windowEnd; 352 } 353 354 windowList.add(finalWindow); 355 356 return ImmutableList.copyOf(windowList); 357 } 358 359 /** 360 * Returns the default early reporting windows 361 * 362 * @param sourceType Source's Type 363 * @param installAttributionEnabled whether windows for install attribution should be provided 364 * @return a list of windows 365 */ 366 public static List<Long> getDefaultEarlyReportingWindowEnds( 367 Source.SourceType sourceType, boolean installAttributionEnabled) { 368 long[] earlyWindows; 369 if (installAttributionEnabled) { 370 earlyWindows = 371 sourceType == Source.SourceType.EVENT 372 ? INSTALL_ATTR_EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS 373 : INSTALL_ATTR_NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS; 374 } else { 375 earlyWindows = 376 sourceType == Source.SourceType.EVENT 377 ? EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS 378 : NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS; 379 } 380 return asList(earlyWindows); 381 } 382 383 /** 384 * Returns default or configured (via flag) early reporting windows for the SourceType 385 * 386 * @param sourceType Source's Type 387 * @param defaultEarlyWindows default value for early windows 388 * @return list of windows 389 */ 390 public List<Long> getConfiguredOrDefaultEarlyReportingWindowEnds( 391 Source.SourceType sourceType, List<Long> defaultEarlyWindowEnds) { 392 // `defaultEarlyWindowEnds` may contain custom install-related logic, which we only apply if 393 // the configurable report windows (and max reports) are in their default state. Without 394 // this check, we may construct default-value report windows without the custom 395 // install-related logic applied. 396 if ((sourceType == Source.SourceType.EVENT && isDefaultConfiguredVtc()) 397 || (sourceType == Source.SourceType.NAVIGATION && isDefaultConfiguredCtc())) { 398 return defaultEarlyWindowEnds; 399 } 400 401 String earlyReportingWindowsString = pickEarlyReportingWindowsConfig(mFlags, sourceType); 402 403 if (earlyReportingWindowsString.isEmpty()) { 404 // No early reporting windows specified. It needs to be handled separately because 405 // splitting an empty string results in an array containing a single empty string. We 406 // want to handle it as an empty array. 407 return Collections.emptyList(); 408 } 409 410 ImmutableList.Builder<Long> earlyWindowEnds = new ImmutableList.Builder<>(); 411 String[] split = 412 earlyReportingWindowsString.split(EARLY_REPORTING_WINDOWS_CONFIG_DELIMITER); 413 if (split.length > MAX_CONFIGURABLE_EVENT_REPORT_EARLY_REPORTING_WINDOWS) { 414 LoggerFactory.getMeasurementLogger() 415 .d( 416 "Invalid configurable early reporting window; more than allowed size: " 417 + MAX_CONFIGURABLE_EVENT_REPORT_EARLY_REPORTING_WINDOWS); 418 return defaultEarlyWindowEnds; 419 } 420 421 for (String windowEnd : split) { 422 try { 423 earlyWindowEnds.add(TimeUnit.SECONDS.toMillis(Long.parseLong(windowEnd))); 424 } catch (NumberFormatException e) { 425 LoggerFactory.getMeasurementLogger() 426 .d(e, "Configurable early reporting window parsing failed."); 427 return defaultEarlyWindowEnds; 428 } 429 } 430 return earlyWindowEnds.build(); 431 } 432 433 private Pair<Long, Long> getFinalReportingWindow( 434 Source source, List<Long> earlyWindowEnds) { 435 // The latest end-time we can associate with a report for this source 436 long effectiveExpiry = Math.min( 437 source.getEffectiveEventReportWindow(), source.getExpiryTime()); 438 // Find the latest end-time that can start a window ending at effectiveExpiry 439 for (int i = earlyWindowEnds.size() - 1; i >= 0; i--) { 440 long windowEnd = earlyWindowEnds.get(i); 441 if (windowEnd < effectiveExpiry) { 442 return Pair.create(windowEnd, effectiveExpiry); 443 } 444 } 445 return Pair.create(source.getEventTime(), effectiveExpiry); 446 } 447 448 /** Indicates whether VTC report windows and max reports are default configured, which can 449 * affect custom install-related attribution. 450 */ 451 public boolean isDefaultConfiguredVtc() { 452 return mFlags.getMeasurementEventReportsVtcEarlyReportingWindows().isEmpty() 453 && mFlags.getMeasurementVtcConfigurableMaxEventReportsCount() == 1; 454 } 455 456 /** Indicates whether CTC report windows are default configured, which can affect custom 457 * install-related attribution. 458 */ 459 private boolean isDefaultConfiguredCtc() { 460 return mFlags.getMeasurementEventReportsCtcEarlyReportingWindows().equals( 461 Flags.MEASUREMENT_EVENT_REPORTS_CTC_EARLY_REPORTING_WINDOWS); 462 } 463 464 private static String pickEarlyReportingWindowsConfig(Flags flags, 465 Source.SourceType sourceType) { 466 return sourceType == Source.SourceType.EVENT 467 ? flags.getMeasurementEventReportsVtcEarlyReportingWindows() 468 : flags.getMeasurementEventReportsCtcEarlyReportingWindows(); 469 } 470 471 private static List<Long> asList(long[] values) { 472 final List<Long> list = new ArrayList<>(); 473 for (Long value : values) { 474 list.add(value); 475 } 476 return list; 477 } 478 } 479