summaryrefslogtreecommitdiff
path: root/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportWindowCalcDelegate.java
blob: 93f7913854457125f19a9f88199040483caee068 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.adservices.service.measurement.reporting;

import static com.android.adservices.service.measurement.PrivacyParams.EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS;
import static com.android.adservices.service.measurement.PrivacyParams.INSTALL_ATTR_EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS;
import static com.android.adservices.service.measurement.PrivacyParams.INSTALL_ATTR_NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS;
import static com.android.adservices.service.measurement.PrivacyParams.MAX_CONFIGURABLE_EVENT_REPORT_EARLY_REPORTING_WINDOWS;
import static com.android.adservices.service.measurement.PrivacyParams.NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS;

import android.annotation.NonNull;
import android.util.Pair;

import com.android.adservices.LoggerFactory;
import com.android.adservices.service.Flags;
import com.android.adservices.service.measurement.EventSurfaceType;
import com.android.adservices.service.measurement.PrivacyParams;
import com.android.adservices.service.measurement.Source;
import com.android.adservices.service.measurement.Trigger;
import com.android.adservices.service.measurement.TriggerSpec;
import com.android.adservices.service.measurement.TriggerSpecs;
import com.android.adservices.service.measurement.util.UnsignedLong;

import com.google.common.collect.ImmutableList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/** Does event report window related calculations, e.g. count, reporting time. */
public class EventReportWindowCalcDelegate {
    private static final String EARLY_REPORTING_WINDOWS_CONFIG_DELIMITER = ",";

    private final Flags mFlags;

    public EventReportWindowCalcDelegate(@NonNull Flags flags) {
        mFlags = flags;
    }

    /**
     * Max reports count given the Source object.
     *
     * @param source the Source object
     * @return maximum number of reports allowed
     */
    public int getMaxReportCount(Source source) {
        return getMaxReportCount(source, source.isInstallDetectionEnabled());
    }

    /**
     * Max reports count based on conversion destination type.
     *
     * @param source the Source object
     * @param destinationType destination type
     * @return maximum number of reports allowed
     */
    public int getMaxReportCount(@NonNull Source source, @EventSurfaceType int destinationType) {
        return getMaxReportCount(source, isInstallCase(source, destinationType));
    }

    private int getMaxReportCount(@NonNull Source source, boolean isInstallCase) {
        // TODO(b/290101531): Cleanup flags
        if (mFlags.getMeasurementFlexLiteApiEnabled() && source.getMaxEventLevelReports() != null) {
            return source.getMaxEventLevelReports();
        }

        if (source.getSourceType() == Source.SourceType.EVENT) {
            // Additional report essentially for first open + 1 post install conversion. If there
            // is already more than 1 report allowed, no need to have that additional report.
            if (isInstallCase && !source.hasWebDestinations() && isDefaultConfiguredVtc()) {
                return PrivacyParams.INSTALL_ATTR_EVENT_SOURCE_MAX_REPORTS;
            }
            return mFlags.getMeasurementVtcConfigurableMaxEventReportsCount();
        }

        return PrivacyParams.NAVIGATION_SOURCE_MAX_REPORTS;
    }

    /**
     * Calculates the reporting time based on the {@link Trigger} time, {@link Source}'s expiry and
     * trigger destination type.
     *
     * @return the reporting time
     */
    public long getReportingTime(
            @NonNull Source source, long triggerTime, @EventSurfaceType int destinationType) {
        if (triggerTime < source.getEventTime()) {
            return -1;
        }

        // Cases where source could have both web and app destinations, there if the trigger
        // destination is an app, and it was installed, then installState should be considered true.
        List<Pair<Long, Long>> reportingWindows =
                getEffectiveReportingWindows(source, isInstallCase(source, destinationType));
        for (Pair<Long, Long> window : reportingWindows) {
            if (isWithinWindow(triggerTime, window)) {
                return window.second + mFlags.getMeasurementMinEventReportDelayMillis();
            }
        }

        return -1;
    }

    private boolean isWithinWindow(long time, Pair<Long, Long> window) {
        return window.first <= time && time < window.second;
    }

    /**
     * Enum shows trigger time and source window time relationship. It is used to generate different
     * verbose debug reports.
     */
    public enum MomentPlacement {
        BEFORE,
        AFTER,
        WITHIN;
    }

    /**
     * @param source source for which the window is calculated
     * @param triggerTime time for the trigger
     * @param destinationType trigger destination type
     * @return how trigger time falls in source windows
     */
    public MomentPlacement fallsWithinWindow(
            @NonNull Source source, long triggerTime, @EventSurfaceType int destinationType) {
        List<Pair<Long, Long>> reportingWindows =
                getEffectiveReportingWindows(source, isInstallCase(source, destinationType));
        if (triggerTime < reportingWindows.get(0).first) {
            return MomentPlacement.BEFORE;
        }
        if (triggerTime >= reportingWindows.get(reportingWindows.size() - 1).second) {
            return MomentPlacement.AFTER;
        }
        return MomentPlacement.WITHIN;
    }

    /**
     * Return reporting time by index for noising based on the index
     *
     * @param windowIndex index of the reporting window for which
     * @return reporting time in milliseconds
     */
    public long getReportingTimeForNoising(
            @NonNull Source source, int windowIndex) {
        List<Pair<Long, Long>> reportingWindows = getEffectiveReportingWindows(
                source, source.isInstallDetectionEnabled());
        Pair<Long, Long> finalWindow = reportingWindows.get(reportingWindows.size() - 1);
        // TODO: (b/288646239) remove this check, confirming noising indexing accuracy.
        return windowIndex < reportingWindows.size()
                ? reportingWindows.get(windowIndex).second
                        + mFlags.getMeasurementMinEventReportDelayMillis()
                : finalWindow.second + mFlags.getMeasurementMinEventReportDelayMillis();
    }

    /**
     * Returns effective, that is, the ones that occur before {@link
     * Source#getEffectiveEventReportWindow()}, event reporting windows count for noising cases.
     *
     * @param source source for which the count is requested
     */
    public int getReportingWindowCountForNoising(@NonNull Source source) {
        return getEffectiveReportingWindows(source, source.isInstallDetectionEnabled()).size();
    }

    /**
     * Returns reporting time for noising with flex event API.
     *
     * @param windowIndex window index corresponding to which the reporting time should be returned
     * @param triggerDataIndex trigger data state index
     * @param triggerSpecs flex event trigger specs
     */
    public long getReportingTimeForNoisingFlexEventApi(
            int windowIndex, int triggerDataIndex, TriggerSpecs triggerSpecs) {
        for (TriggerSpec triggerSpec : triggerSpecs.getTriggerSpecs()) {
            triggerDataIndex -= triggerSpec.getTriggerData().size();
            if (triggerDataIndex < 0) {
                return triggerSpec.getEventReportWindowsEnd().get(windowIndex)
                        + mFlags.getMeasurementMinEventReportDelayMillis();
            }
        }
        return 0;
    }

    /**
     * Calculates the reporting time based on the {@link Trigger} time for flexible event report API
     *
     * @param triggerSpecs the report specification to be processed
     * @param sourceRegistrationTime source registration time
     * @param triggerTime trigger time
     * @param triggerData the trigger data
     * @return the reporting time
     */
    public long getFlexEventReportingTime(
            TriggerSpecs triggerSpecs,
            long sourceRegistrationTime,
            long triggerTime,
            UnsignedLong triggerData) {
        if (triggerTime < sourceRegistrationTime) {
            return -1L;
        }
        if (triggerTime
                < triggerSpecs.findReportingStartTimeForTriggerData(triggerData)
                        + sourceRegistrationTime) {
            return -1L;
        }

        List<Long> reportingWindows = triggerSpecs.findReportingEndTimesForTriggerData(triggerData);
        for (Long window : reportingWindows) {
            if (triggerTime <= window + sourceRegistrationTime) {
                return sourceRegistrationTime + window
                        + mFlags.getMeasurementMinEventReportDelayMillis();
            }
        }
        return -1L;
    }

    private static boolean isInstallCase(Source source, @EventSurfaceType int destinationType) {
        return destinationType == EventSurfaceType.APP && source.isInstallAttributed();
    }

    /**
     * If the flag is enabled and the specified report windows are valid, picks from flag controlled
     * configurable early reporting windows. Otherwise, falls back to the values provided in
     * {@code getDefaultEarlyReportingWindowEnds}, which can have install-related custom behaviour.
     * It curtails the windows that occur after {@link Source#getEffectiveEventReportWindow()}
     * because they would effectively be unusable.
     */
    private List<Pair<Long, Long>> getEffectiveReportingWindows(Source source,
            boolean installState) {
        // TODO(b/290221611) Remove early reporting windows from code, only use them for flags.
        if (mFlags.getMeasurementFlexLiteApiEnabled() && source.hasManualEventReportWindows()) {
            return source.parsedProcessedEventReportWindows();
        }
        List<Long> defaultEarlyWindowEnds =
                getDefaultEarlyReportingWindowEnds(
                        source.getSourceType(),
                        installState && !source.hasWebDestinations());
        List<Long> earlyWindowEnds =
                getConfiguredOrDefaultEarlyReportingWindowEnds(
                        source.getSourceType(), defaultEarlyWindowEnds);
        // Add source event time to windows
        earlyWindowEnds =
                earlyWindowEnds.stream()
                        .map((x) -> source.getEventTime() + x)
                        .collect(Collectors.toList());

        List<Pair<Long, Long>> windowList = new ArrayList<>();
        long windowStart = 0L;
        Pair<Long, Long> finalWindow =
                getFinalReportingWindow(source, earlyWindowEnds);

        for (long windowEnd : earlyWindowEnds) {
            // Start time of `finalWindow` is either 0 or one of `earlyWindowEnds` times; stop
            // iterating if we see it, and add `finalWindow`.
            if (windowStart == finalWindow.first) {
                break;
            }
            windowList.add(Pair.create(windowStart, windowEnd));
            windowStart = windowEnd;
        }

        windowList.add(finalWindow);

        return ImmutableList.copyOf(windowList);
    }

    /**
     * Returns the default early reporting windows
     *
     * @param sourceType Source's Type
     * @param installAttributionEnabled whether windows for install attribution should be provided
     * @return a list of windows
     */
    public static List<Long> getDefaultEarlyReportingWindowEnds(
            Source.SourceType sourceType, boolean installAttributionEnabled) {
        long[] earlyWindows;
        if (installAttributionEnabled) {
            earlyWindows =
                    sourceType == Source.SourceType.EVENT
                            ? INSTALL_ATTR_EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS
                            : INSTALL_ATTR_NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS;
        } else {
            earlyWindows =
                    sourceType == Source.SourceType.EVENT
                            ? EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS
                            : NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS;
        }
        return asList(earlyWindows);
    }

    /**
     * Returns default or configured (via flag) early reporting windows for the SourceType
     *
     * @param sourceType Source's Type
     * @param defaultEarlyWindows default value for early windows
     * @return list of windows
     */
    public List<Long> getConfiguredOrDefaultEarlyReportingWindowEnds(
            Source.SourceType sourceType, List<Long> defaultEarlyWindowEnds) {
        // `defaultEarlyWindowEnds` may contain custom install-related logic, which we only apply if
        // the configurable report windows (and max reports) are in their default state. Without
        // this check, we may construct default-value report windows without the custom
        // install-related logic applied.
        if ((sourceType == Source.SourceType.EVENT && isDefaultConfiguredVtc())
                || (sourceType == Source.SourceType.NAVIGATION && isDefaultConfiguredCtc())) {
            return defaultEarlyWindowEnds;
        }

        String earlyReportingWindowsString = pickEarlyReportingWindowsConfig(mFlags, sourceType);

        if (earlyReportingWindowsString.isEmpty()) {
            // No early reporting windows specified. It needs to be handled separately because
            // splitting an empty string results in an array containing a single empty string. We
            // want to handle it as an empty array.
            return Collections.emptyList();
        }

        ImmutableList.Builder<Long> earlyWindowEnds = new ImmutableList.Builder<>();
        String[] split =
                earlyReportingWindowsString.split(EARLY_REPORTING_WINDOWS_CONFIG_DELIMITER);
        if (split.length > MAX_CONFIGURABLE_EVENT_REPORT_EARLY_REPORTING_WINDOWS) {
            LoggerFactory.getMeasurementLogger()
                    .d(
                            "Invalid configurable early reporting window; more than allowed size: "
                                    + MAX_CONFIGURABLE_EVENT_REPORT_EARLY_REPORTING_WINDOWS);
            return defaultEarlyWindowEnds;
        }

        for (String windowEnd : split) {
            try {
                earlyWindowEnds.add(TimeUnit.SECONDS.toMillis(Long.parseLong(windowEnd)));
            } catch (NumberFormatException e) {
                LoggerFactory.getMeasurementLogger()
                        .d(e, "Configurable early reporting window parsing failed.");
                return defaultEarlyWindowEnds;
            }
        }
        return earlyWindowEnds.build();
    }

    private Pair<Long, Long> getFinalReportingWindow(
            Source source, List<Long> earlyWindowEnds) {
        // The latest end-time we can associate with a report for this source
        long effectiveExpiry = Math.min(
                source.getEffectiveEventReportWindow(), source.getExpiryTime());
        // Find the latest end-time that can start a window ending at effectiveExpiry
        for (int i = earlyWindowEnds.size() - 1; i >= 0; i--) {
            long windowEnd = earlyWindowEnds.get(i);
            if (windowEnd < effectiveExpiry) {
                return Pair.create(windowEnd, effectiveExpiry);
            }
        }
        return Pair.create(0L, effectiveExpiry);
    }

    /** Indicates whether VTC report windows and max reports are default configured, which can
     * affect custom install-related attribution.
     */
    public boolean isDefaultConfiguredVtc() {
        return mFlags.getMeasurementEventReportsVtcEarlyReportingWindows().isEmpty()
                && mFlags.getMeasurementVtcConfigurableMaxEventReportsCount() == 1;
    }

    /** Indicates whether CTC report windows are default configured, which can affect custom
     * install-related attribution.
     */
    private boolean isDefaultConfiguredCtc() {
        return mFlags.getMeasurementEventReportsCtcEarlyReportingWindows().equals(
                Flags.MEASUREMENT_EVENT_REPORTS_CTC_EARLY_REPORTING_WINDOWS);
    }

    private static String pickEarlyReportingWindowsConfig(Flags flags,
            Source.SourceType sourceType) {
        return sourceType == Source.SourceType.EVENT
                ? flags.getMeasurementEventReportsVtcEarlyReportingWindows()
                : flags.getMeasurementEventReportsCtcEarlyReportingWindows();
    }

    private static List<Long> asList(long[] values) {
        final List<Long> list = new ArrayList<>();
        for (Long value : values) {
            list.add(value);
        }
        return list;
    }
}