• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.util.Pair;
22 
23 import com.android.adservices.data.measurement.DatastoreException;
24 import com.android.adservices.data.measurement.IMeasurementDao;
25 import com.android.adservices.service.Flags;
26 import com.android.adservices.service.FlagsFactory;
27 import com.android.adservices.service.common.AllowLists;
28 import com.android.adservices.service.measurement.EventSurfaceType;
29 import com.android.adservices.service.measurement.Source;
30 import com.android.adservices.service.measurement.Trigger;
31 import com.android.adservices.service.measurement.util.UnsignedLong;
32 import com.android.adservices.service.stats.AdServicesLogger;
33 import com.android.adservices.service.stats.AdServicesLoggerImpl;
34 import com.android.adservices.service.stats.MsmtDebugKeysMatchStats;
35 
36 import com.google.common.annotations.VisibleForTesting;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.HashSet;
41 import java.util.Objects;
42 import java.util.Set;
43 
44 /** Util class for DebugKeys */
45 public class DebugKeyAccessor {
46     @NonNull private final Flags mFlags;
47     @NonNull private final AdServicesLogger mAdServicesLogger;
48     @NonNull private final IMeasurementDao mMeasurementDao;
49 
DebugKeyAccessor(IMeasurementDao measurementDao)50     public DebugKeyAccessor(IMeasurementDao measurementDao) {
51         this(FlagsFactory.getFlags(), AdServicesLoggerImpl.getInstance(), measurementDao);
52     }
53 
54     @VisibleForTesting
DebugKeyAccessor( @onNull Flags flags, @NonNull AdServicesLogger adServicesLogger, @NonNull IMeasurementDao measurementDao)55     DebugKeyAccessor(
56             @NonNull Flags flags,
57             @NonNull AdServicesLogger adServicesLogger,
58             @NonNull IMeasurementDao measurementDao) {
59         mFlags = flags;
60         mAdServicesLogger = adServicesLogger;
61         mMeasurementDao = measurementDao;
62     }
63 
64     /**
65      * This is kept in sync with the match type codes in {@link
66      * com.android.adservices.service.stats.AdServicesStatsLog}.
67      */
68     @IntDef(
69             value = {
70                 AttributionType.UNKNOWN,
71                 AttributionType.SOURCE_APP_TRIGGER_APP,
72                 AttributionType.SOURCE_APP_TRIGGER_WEB,
73                 AttributionType.SOURCE_WEB_TRIGGER_APP,
74                 AttributionType.SOURCE_WEB_TRIGGER_WEB
75             })
76     @Retention(RetentionPolicy.SOURCE)
77     public @interface AttributionType {
78         int UNKNOWN = 0;
79         int SOURCE_APP_TRIGGER_APP = 1;
80         int SOURCE_APP_TRIGGER_WEB = 2;
81         int SOURCE_WEB_TRIGGER_APP = 3;
82         int SOURCE_WEB_TRIGGER_WEB = 4;
83     }
84 
85     /** Returns DebugKey according to the permissions set */
getDebugKeys(Source source, Trigger trigger)86     public Pair<UnsignedLong, UnsignedLong> getDebugKeys(Source source, Trigger trigger)
87             throws DatastoreException {
88         Set<String> allowedEnrollmentsString =
89                 new HashSet<>(
90                         AllowLists.splitAllowList(
91                                 mFlags.getMeasurementDebugJoinKeyEnrollmentAllowlist()));
92         String blockedEnrollmentsAdIdMatchingString =
93                 mFlags.getMeasurementPlatformDebugAdIdMatchingEnrollmentBlocklist();
94         Set<String> blockedEnrollmentsAdIdMatching =
95                 new HashSet<>(AllowLists.splitAllowList(blockedEnrollmentsAdIdMatchingString));
96         UnsignedLong sourceDebugKey = null;
97         UnsignedLong triggerDebugKey = null;
98         Long joinKeyHash = null;
99         @AttributionType int attributionType = getAttributionType(source, trigger);
100         boolean doDebugJoinKeysMatch = false;
101         switch (attributionType) {
102             case AttributionType.SOURCE_APP_TRIGGER_APP:
103                 if (source.hasAdIdPermission()) {
104                     sourceDebugKey = source.getDebugKey();
105                 }
106                 if (trigger.hasAdIdPermission()) {
107                     triggerDebugKey = trigger.getDebugKey();
108                 }
109                 break;
110             case AttributionType.SOURCE_WEB_TRIGGER_WEB:
111                 // TODO(b/280323940): Web<>Web Debug Keys AdID option
112                 if (trigger.getRegistrant().equals(source.getRegistrant())) {
113                     if (source.hasArDebugPermission()) {
114                         sourceDebugKey = source.getDebugKey();
115                     }
116                     if (trigger.hasArDebugPermission()) {
117                         triggerDebugKey = trigger.getDebugKey();
118                     }
119                 } else if (canMatchJoinKeys(source, trigger, allowedEnrollmentsString)) {
120                     // Attempted to match, so assigning a non-null value to emit metric
121                     joinKeyHash = 0L;
122                     if (source.getDebugJoinKey().equals(trigger.getDebugJoinKey())) {
123                         sourceDebugKey = source.getDebugKey();
124                         triggerDebugKey = trigger.getDebugKey();
125                         joinKeyHash = (long) source.getDebugJoinKey().hashCode();
126                         doDebugJoinKeysMatch = true;
127                     }
128                 }
129                 break;
130             case AttributionType.SOURCE_APP_TRIGGER_WEB:
131                 if (canMatchAdIdAppSourceToWebTrigger(source, trigger)
132                         && canMatchAdIdEnrollments(
133                                 source,
134                                 trigger,
135                                 blockedEnrollmentsAdIdMatchingString,
136                                 blockedEnrollmentsAdIdMatching)) {
137                     if (source.getPlatformAdId().equals(trigger.getDebugAdId())
138                             && isEnrollmentIdWithinUniqueAdIdLimit(trigger.getEnrollmentId())) {
139                         sourceDebugKey = source.getDebugKey();
140                         triggerDebugKey = trigger.getDebugKey();
141                     }
142                     // TODO(b/280322027): Record result for metrics emission.
143                     break;
144                 }
145                 // fall-through for join key matching
146             case AttributionType.SOURCE_WEB_TRIGGER_APP:
147                 if (canMatchAdIdWebSourceToAppTrigger(source, trigger)
148                         && canMatchAdIdEnrollments(
149                                 source,
150                                 trigger,
151                                 blockedEnrollmentsAdIdMatchingString,
152                                 blockedEnrollmentsAdIdMatching)) {
153                     if (trigger.getPlatformAdId().equals(source.getDebugAdId())
154                             && isEnrollmentIdWithinUniqueAdIdLimit(source.getEnrollmentId())) {
155                         sourceDebugKey = source.getDebugKey();
156                         triggerDebugKey = trigger.getDebugKey();
157                     }
158                     // TODO(b/280322027): Record result for metrics emission.
159                     break;
160                 }
161                 if (canMatchJoinKeys(source, trigger, allowedEnrollmentsString)) {
162                     // Attempted to match, so assigning a non-null value to emit metric
163                     joinKeyHash = 0L;
164                     if (source.getDebugJoinKey().equals(trigger.getDebugJoinKey())) {
165                         sourceDebugKey = source.getDebugKey();
166                         triggerDebugKey = trigger.getDebugKey();
167                         joinKeyHash = (long) source.getDebugJoinKey().hashCode();
168                         doDebugJoinKeysMatch = true;
169                     }
170                 }
171                 break;
172             case AttributionType.UNKNOWN:
173                 // fall-through
174             default:
175                 break;
176         }
177         logDebugKeysMatch(
178                 joinKeyHash, trigger, attributionType, doDebugJoinKeysMatch, mAdServicesLogger);
179         return new Pair<>(sourceDebugKey, triggerDebugKey);
180     }
181 
182     /** Returns DebugKey according to the permissions set */
getDebugKeysForVerboseTriggerDebugReport( @onNull Source source, Trigger trigger)183     public Pair<UnsignedLong, UnsignedLong> getDebugKeysForVerboseTriggerDebugReport(
184             @NonNull Source source, Trigger trigger) throws DatastoreException {
185         if (source == null) {
186             if (trigger.getDestinationType() == EventSurfaceType.WEB
187                     && trigger.hasArDebugPermission()) {
188                 return new Pair<>(null, trigger.getDebugKey());
189             } else if (trigger.getDestinationType() == EventSurfaceType.APP
190                     && trigger.hasAdIdPermission()) {
191                 return new Pair<>(null, trigger.getDebugKey());
192             } else {
193                 return new Pair<>(null, null);
194             }
195         }
196         Set<String> allowedEnrollmentsString =
197                 new HashSet<>(
198                         AllowLists.splitAllowList(
199                                 mFlags.getMeasurementDebugJoinKeyEnrollmentAllowlist()));
200         String blockedEnrollmentsAdIdMatchingString =
201                 mFlags.getMeasurementPlatformDebugAdIdMatchingEnrollmentBlocklist();
202         Set<String> blockedEnrollmentsAdIdMatching =
203                 new HashSet<>(AllowLists.splitAllowList(blockedEnrollmentsAdIdMatchingString));
204         UnsignedLong sourceDebugKey = null;
205         UnsignedLong triggerDebugKey = null;
206         Long joinKeyHash = null;
207         @AttributionType int attributionType = getAttributionType(source, trigger);
208         boolean doDebugJoinKeysMatch = false;
209         switch (attributionType) {
210             case AttributionType.SOURCE_APP_TRIGGER_APP:
211                 // Gated on Trigger Adid permission.
212                 if (!trigger.hasAdIdPermission()) {
213                     break;
214                 }
215                 triggerDebugKey = trigger.getDebugKey();
216                 if (source.hasAdIdPermission()) {
217                     sourceDebugKey = source.getDebugKey();
218                 }
219                 break;
220             case AttributionType.SOURCE_WEB_TRIGGER_WEB:
221                 // Gated on Trigger ar_debug permission.
222                 if (!trigger.hasArDebugPermission()) {
223                     break;
224                 }
225                 triggerDebugKey = trigger.getDebugKey();
226                 if (trigger.getRegistrant().equals(source.getRegistrant())) {
227                     if (source.hasArDebugPermission()) {
228                         sourceDebugKey = source.getDebugKey();
229                     }
230                 } else {
231                     // Send source_debug_key when condition meets.
232                     if (canMatchJoinKeys(source, trigger, allowedEnrollmentsString)) {
233                         // Attempted to match, so assigning a non-null value to emit metric
234                         joinKeyHash = 0L;
235                         if (source.getDebugJoinKey().equals(trigger.getDebugJoinKey())) {
236                             sourceDebugKey = source.getDebugKey();
237                             joinKeyHash = (long) source.getDebugJoinKey().hashCode();
238                             doDebugJoinKeysMatch = true;
239                         }
240                     }
241                 }
242                 break;
243             case AttributionType.SOURCE_APP_TRIGGER_WEB:
244                 // Gated on Trigger ar_debug permission.
245                 if (!trigger.hasArDebugPermission()) {
246                     break;
247                 }
248                 triggerDebugKey = trigger.getDebugKey();
249                 // Send source_debug_key when condition meets.
250                 if (canMatchAdIdAppSourceToWebTrigger(source, trigger)
251                         && canMatchAdIdEnrollments(
252                                 source,
253                                 trigger,
254                                 blockedEnrollmentsAdIdMatchingString,
255                                 blockedEnrollmentsAdIdMatching)) {
256                     if (source.getPlatformAdId().equals(trigger.getDebugAdId())
257                             && isEnrollmentIdWithinUniqueAdIdLimit(trigger.getEnrollmentId())) {
258                         sourceDebugKey = source.getDebugKey();
259                     }
260                     // TODO(b/280322027): Record result for metrics emission.
261                 } else if (canMatchJoinKeys(source, trigger, allowedEnrollmentsString)) {
262                     // Attempted to match, so assigning a non-null value to emit metric
263                     joinKeyHash = 0L;
264                     if (source.getDebugJoinKey().equals(trigger.getDebugJoinKey())) {
265                         sourceDebugKey = source.getDebugKey();
266                         joinKeyHash = (long) source.getDebugJoinKey().hashCode();
267                         doDebugJoinKeysMatch = true;
268                     }
269                 }
270                 break;
271             case AttributionType.SOURCE_WEB_TRIGGER_APP:
272                 // Gated on Trigger Adid permission.
273                 if (!trigger.hasAdIdPermission()) {
274                     break;
275                 }
276                 triggerDebugKey = trigger.getDebugKey();
277                 // Send source_debug_key when condition meets.
278                 if (canMatchAdIdWebSourceToAppTrigger(source, trigger)
279                         && canMatchAdIdEnrollments(
280                                 source,
281                                 trigger,
282                                 blockedEnrollmentsAdIdMatchingString,
283                                 blockedEnrollmentsAdIdMatching)) {
284                     if (trigger.getPlatformAdId().equals(source.getDebugAdId())
285                             && isEnrollmentIdWithinUniqueAdIdLimit(source.getEnrollmentId())) {
286                         sourceDebugKey = source.getDebugKey();
287                     }
288                     // TODO(b/280322027): Record result for metrics emission.
289                 } else if (canMatchJoinKeys(source, trigger, allowedEnrollmentsString)) {
290                     // Attempted to match, so assigning a non-null value to emit metric
291                     joinKeyHash = 0L;
292                     if (source.getDebugJoinKey().equals(trigger.getDebugJoinKey())) {
293                         sourceDebugKey = source.getDebugKey();
294                         joinKeyHash = (long) source.getDebugJoinKey().hashCode();
295                         doDebugJoinKeysMatch = true;
296                     }
297                 }
298                 break;
299             case AttributionType.UNKNOWN:
300                 // fall-through
301             default:
302                 break;
303         }
304         logDebugKeysMatch(
305                 joinKeyHash, trigger, attributionType, doDebugJoinKeysMatch, mAdServicesLogger);
306         return new Pair<>(sourceDebugKey, triggerDebugKey);
307     }
308 
logDebugKeysMatch( Long joinKeyHash, Trigger trigger, int attributionType, boolean doDebugJoinKeysMatch, AdServicesLogger mAdServicesLogger)309     private void logDebugKeysMatch(
310             Long joinKeyHash,
311             Trigger trigger,
312             int attributionType,
313             boolean doDebugJoinKeysMatch,
314             AdServicesLogger mAdServicesLogger) {
315         long debugKeyHashLimit = mFlags.getMeasurementDebugJoinKeyHashLimit();
316         // The provided hash limit is valid and the join key was attempted to be matched.
317         if (debugKeyHashLimit > 0 && joinKeyHash != null) {
318             long hashedValue = joinKeyHash % debugKeyHashLimit;
319             MsmtDebugKeysMatchStats stats =
320                     MsmtDebugKeysMatchStats.builder()
321                             .setAdTechEnrollmentId(trigger.getEnrollmentId())
322                             .setAttributionType(attributionType)
323                             .setMatched(doDebugJoinKeysMatch)
324                             .setDebugJoinKeyHashedValue(hashedValue)
325                             .setDebugJoinKeyHashLimit(debugKeyHashLimit)
326                             .build();
327             mAdServicesLogger.logMeasurementDebugKeysMatch(stats);
328         }
329     }
canMatchJoinKeys( Source source, Trigger trigger, Set<String> allowedEnrollmentsString)330     private static boolean canMatchJoinKeys(
331             Source source, Trigger trigger, Set<String> allowedEnrollmentsString) {
332         return allowedEnrollmentsString.contains(trigger.getEnrollmentId())
333                 && allowedEnrollmentsString.contains(source.getEnrollmentId())
334                 && Objects.nonNull(source.getDebugJoinKey())
335                 && Objects.nonNull(trigger.getDebugJoinKey());
336     }
337 
canMatchAdIdEnrollments( Source source, Trigger trigger, String blockedEnrollmentsString, Set<String> blockedEnrollments)338     private boolean canMatchAdIdEnrollments(
339             Source source,
340             Trigger trigger,
341             String blockedEnrollmentsString,
342             Set<String> blockedEnrollments) {
343         return !AllowLists.doesAllowListAllowAll(blockedEnrollmentsString)
344                 && !blockedEnrollments.contains(source.getEnrollmentId())
345                 && !blockedEnrollments.contains(trigger.getEnrollmentId());
346     }
347 
canMatchAdIdAppSourceToWebTrigger(Source source, Trigger trigger)348     private static boolean canMatchAdIdAppSourceToWebTrigger(Source source, Trigger trigger) {
349         return trigger.hasArDebugPermission()
350                 && Objects.nonNull(source.getPlatformAdId())
351                 && Objects.nonNull(trigger.getDebugAdId());
352     }
353 
canMatchAdIdWebSourceToAppTrigger(Source source, Trigger trigger)354     private static boolean canMatchAdIdWebSourceToAppTrigger(Source source, Trigger trigger) {
355         return source.hasArDebugPermission()
356                 && Objects.nonNull(source.getDebugAdId())
357                 && Objects.nonNull(trigger.getPlatformAdId());
358     }
359 
isEnrollmentIdWithinUniqueAdIdLimit(String enrollmentId)360     private boolean isEnrollmentIdWithinUniqueAdIdLimit(String enrollmentId)
361             throws DatastoreException {
362         long numUnique = mMeasurementDao.countDistinctDebugAdIdsUsedByEnrollment(enrollmentId);
363         return numUnique < mFlags.getMeasurementPlatformDebugAdIdMatchingLimit();
364     }
365 
366     @AttributionType
getAttributionType(Source source, Trigger trigger)367     private static int getAttributionType(Source source, Trigger trigger) {
368         boolean isSourceApp = source.getPublisherType() == EventSurfaceType.APP;
369         if (trigger.getDestinationType() == EventSurfaceType.WEB) {
370             // Web Conversion
371             return isSourceApp
372                     ? AttributionType.SOURCE_APP_TRIGGER_WEB
373                     : AttributionType.SOURCE_WEB_TRIGGER_WEB;
374         } else {
375             // App Conversion
376             return isSourceApp
377                     ? AttributionType.SOURCE_APP_TRIGGER_APP
378                     : AttributionType.SOURCE_WEB_TRIGGER_APP;
379         }
380     }
381 }
382