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