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