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; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.util.Pair; 22 23 import com.android.adservices.LoggerFactory; 24 import com.android.adservices.service.Flags; 25 import com.android.adservices.service.measurement.noising.Combinatorics; 26 import com.android.adservices.service.measurement.util.UnsignedLong; 27 import com.android.internal.annotations.VisibleForTesting; 28 29 import org.json.JSONArray; 30 import org.json.JSONException; 31 import org.json.JSONObject; 32 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Comparator; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Objects; 40 41 /** 42 * A class wrapper for the trigger specification from the input argument during source registration 43 */ 44 public class TriggerSpecs { 45 private final TriggerSpec[] mTriggerSpecs; 46 private int mMaxEventLevelReports; 47 private PrivacyComputationParams mPrivacyParams; 48 private final Map<UnsignedLong, Integer> mTriggerDataToTriggerSpecIndexMap = new HashMap<>(); 49 // Reference to a list that is a property of the Source object. 50 private List<AttributedTrigger> mAttributedTriggersRef; 51 52 // Trigger data magnitude is restricted to 32 bits. 53 public static final UnsignedLong MAX_TRIGGER_DATA_VALUE = new UnsignedLong((1L << 32) - 1L); 54 // Max bucket threshold is 32 bits. 55 public static final long MAX_BUCKET_THRESHOLD = (1L << 32) - 1L; 56 57 /** The JSON keys for flexible event report API input */ 58 public interface FlexEventReportJsonKeys { 59 String VALUE = "value"; 60 String PRIORITY = "priority"; 61 String TRIGGER_TIME = "trigger_time"; 62 String TRIGGER_DATA = "trigger_data"; 63 String FLIP_PROBABILITY = "flip_probability"; 64 String END_TIMES = "end_times"; 65 String START_TIME = "start_time"; 66 String SUMMARY_WINDOW_OPERATOR = "summary_window_operator"; 67 String EVENT_REPORT_WINDOWS = "event_report_windows"; 68 String SUMMARY_BUCKETS = "summary_buckets"; 69 } 70 TriggerSpecs( String triggerSpecsString, String maxEventLevelReports, Source source, String privacyParametersString)71 public TriggerSpecs( 72 String triggerSpecsString, 73 String maxEventLevelReports, 74 Source source, 75 String privacyParametersString) 76 throws JSONException { 77 this( 78 triggerSpecsString, 79 Integer.parseInt(maxEventLevelReports), 80 source, 81 privacyParametersString); 82 } 83 84 /** 85 * This constructor is called during the attribution process. Current trigger status will be 86 * read and process to determine the outcome of incoming trigger. 87 * 88 * @param triggerSpecsString input trigger specs from ad tech 89 * @param maxEventLevelReports max event level reports from ad tech 90 * @param source the source associated with this trigger specification 91 * @param privacyParametersString computed privacy parameters 92 * @throws JSONException JSON exception 93 */ TriggerSpecs( String triggerSpecsString, int maxEventLevelReports, @Nullable Source source, String privacyParametersString)94 public TriggerSpecs( 95 String triggerSpecsString, 96 int maxEventLevelReports, 97 @Nullable Source source, 98 String privacyParametersString) 99 throws JSONException { 100 if (triggerSpecsString == null || triggerSpecsString.isEmpty()) { 101 throw new JSONException("the source is not registered as flexible event report API"); 102 } 103 JSONArray triggerSpecs = new JSONArray(triggerSpecsString); 104 mTriggerSpecs = new TriggerSpec[triggerSpecs.length()]; 105 for (int i = 0; i < triggerSpecs.length(); i++) { 106 mTriggerSpecs[i] = new TriggerSpec.Builder(triggerSpecs.getJSONObject(i)).build(); 107 for (UnsignedLong triggerData : mTriggerSpecs[i].getTriggerData()) { 108 mTriggerDataToTriggerSpecIndexMap.put(triggerData, i); 109 } 110 } 111 112 mMaxEventLevelReports = maxEventLevelReports; 113 114 if (source != null) { 115 mAttributedTriggersRef = source.getAttributedTriggers(); 116 } 117 118 mPrivacyParams = new PrivacyComputationParams(privacyParametersString); 119 } 120 121 /** 122 * This constructor is called during the source registration process. 123 * 124 * @param triggerSpecs trigger specs from ad tech 125 * @param maxEventLevelReports max event level reports from ad tech 126 * @param source the {@code Source} associated with this trigger specification 127 */ TriggerSpecs(@onNull TriggerSpec[] triggerSpecs, int maxEventLevelReports, Source source)128 public TriggerSpecs(@NonNull TriggerSpec[] triggerSpecs, int maxEventLevelReports, 129 Source source) { 130 mTriggerSpecs = triggerSpecs; 131 mMaxEventLevelReports = maxEventLevelReports; 132 if (source != null) { 133 mAttributedTriggersRef = source.getAttributedTriggers(); 134 } 135 for (int i = 0; i < triggerSpecs.length; i++) { 136 for (UnsignedLong triggerData : triggerSpecs[i].getTriggerData()) { 137 mTriggerDataToTriggerSpecIndexMap.put(triggerData, i); 138 } 139 } 140 } 141 142 /** 143 * @return the information gain 144 */ getInformationGain(Source source, Flags flags)145 public double getInformationGain(Source source, Flags flags) { 146 if (mPrivacyParams == null) { 147 buildPrivacyParameters(source, flags); 148 } 149 return mPrivacyParams.getInformationGain(); 150 } 151 152 /** @return whether the trigger specs have a valid report state count */ hasValidReportStateCount(Source source, Flags flags)153 public boolean hasValidReportStateCount(Source source, Flags flags) { 154 if (mPrivacyParams == null) { 155 buildPrivacyParameters(source, flags); 156 } 157 return mPrivacyParams.hasValidReportStateCount(); 158 } 159 160 /** 161 * @return The number of report state counts for the trigger specifications, or 0 if invalid. 162 */ getNumStates(Source source, Flags flags)163 public long getNumStates(Source source, Flags flags) { 164 if (mPrivacyParams == null) { 165 buildPrivacyParameters(source, flags); 166 } 167 return mPrivacyParams.getNumStates(); 168 } 169 170 /** @return the probability to use fake report */ getFlipProbability(Source source, Flags flags)171 public double getFlipProbability(Source source, Flags flags) { 172 if (mPrivacyParams == null) { 173 buildPrivacyParameters(source, flags); 174 } 175 return mPrivacyParams.getFlipProbability(); 176 } 177 178 /** 179 * Get the parameters for the privacy computation. 1st element: total report cap, an array with 180 * 1 element is used to store the integer; 2nd element: number of windows per trigger data type; 181 * 3rd element: number of report cap per trigger data type. 182 * 183 * @return the parameters to computer number of states and fake report 184 */ getPrivacyParamsForComputation()185 public int[][] getPrivacyParamsForComputation() { 186 // TODO (b/313920181): build privacy params in case null. 187 int[][] params = new int[3][]; 188 params[0] = new int[] {mMaxEventLevelReports}; 189 params[1] = mPrivacyParams.getPerTypeNumWindowList(); 190 params[2] = mPrivacyParams.getPerTypeCapList(); 191 return params; 192 } 193 194 /** 195 * getter method for mTriggerSpecs 196 * 197 * @return the array of TriggerSpec 198 */ getTriggerSpecs()199 public TriggerSpec[] getTriggerSpecs() { 200 return mTriggerSpecs; 201 } 202 203 /** 204 * @return Max number of reports) 205 */ getMaxReports()206 public int getMaxReports() { 207 return mMaxEventLevelReports; 208 } 209 210 /** 211 * Get the trigger datum given a trigger datum index. In the flexible event API, the trigger 212 * data are distributed uniquely among the trigger spec objects. 213 * 214 * @param triggerDataIndex The index of the triggerData 215 * @return the trigger data 216 */ getTriggerDataFromIndex(int triggerDataIndex)217 public UnsignedLong getTriggerDataFromIndex(int triggerDataIndex) { 218 for (TriggerSpec triggerSpec : mTriggerSpecs) { 219 int prevTriggerDataIndex = triggerDataIndex; 220 triggerDataIndex -= triggerSpec.getTriggerData().size(); 221 if (triggerDataIndex < 0) { 222 return triggerSpec.getTriggerData().get(prevTriggerDataIndex); 223 } 224 } 225 // will not reach here 226 return null; 227 } 228 229 /** 230 * @param index the index of the summary bucket 231 * @param summaryBuckets the summary bucket 232 * @return return single summary bucket of the index 233 */ getSummaryBucketFromIndex( int index, @NonNull List<Long> summaryBuckets)234 public static Pair<Long, Long> getSummaryBucketFromIndex( 235 int index, @NonNull List<Long> summaryBuckets) { 236 return new Pair<>( 237 summaryBuckets.get(index), 238 index < summaryBuckets.size() - 1 239 ? summaryBuckets.get(index + 1) - 1 240 : MAX_BUCKET_THRESHOLD); 241 } 242 243 /** 244 * @param triggerData the trigger data 245 * @return the summary bucket configured for the trigger data 246 */ getSummaryBucketsForTriggerData(UnsignedLong triggerData)247 public List<Long> getSummaryBucketsForTriggerData(UnsignedLong triggerData) { 248 int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData); 249 return mTriggerSpecs[index].getSummaryBuckets(); 250 } 251 252 /** 253 * @param triggerData the trigger data 254 * @return the summary operator type configured for the trigger data 255 */ getSummaryOperatorType(UnsignedLong triggerData)256 public TriggerSpec.SummaryOperatorType getSummaryOperatorType(UnsignedLong triggerData) { 257 int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData); 258 return mTriggerSpecs[index].getSummaryWindowOperator(); 259 } 260 261 /** 262 * @param triggerData the trigger data 263 * @return the event report windows start time configured for the trigger data 264 */ findReportingStartTimeForTriggerData(UnsignedLong triggerData)265 public Long findReportingStartTimeForTriggerData(UnsignedLong triggerData) { 266 int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData); 267 return mTriggerSpecs[index].getEventReportWindowsStart(); 268 } 269 270 /** 271 * @param triggerData the trigger data 272 * @return the event report window ends configured for the trigger data 273 */ findReportingEndTimesForTriggerData(UnsignedLong triggerData)274 public List<Long> findReportingEndTimesForTriggerData(UnsignedLong triggerData) { 275 int index = mTriggerDataToTriggerSpecIndexMap.get(triggerData); 276 return mTriggerSpecs[index].getEventReportWindowsEnd(); 277 } 278 279 /** 280 * Prepares structures for flex attribution handling. 281 * 282 * @param sourceEventReports delivered and pending reports for the source 283 * @param triggerTime trigger time 284 * @param reportsToDelete a list that the method will populate with reports to delete 285 * @param triggerDataToBucketIndexMap a map that the method will populate with the current 286 * bucket index per trigger data after considering delivered reports 287 */ prepareFlexAttribution( List<EventReport> sourceEventReports, long triggerTime, List<EventReport> reportsToDelete, Map<UnsignedLong, Integer> triggerDataToBucketIndexMap)288 public void prepareFlexAttribution( 289 List<EventReport> sourceEventReports, 290 long triggerTime, 291 List<EventReport> reportsToDelete, 292 Map<UnsignedLong, Integer> triggerDataToBucketIndexMap) { 293 // Completed reports represent an ordered sequence of summary buckets. 294 sourceEventReports.sort( 295 Comparator.comparing(EventReport::getTriggerData) 296 .thenComparing(Comparator.comparingLong( 297 eventReport -> eventReport.getTriggerSummaryBucket().first))); 298 299 // Iterate over completed reports and store for each attributed trigger its contribution. 300 // Also record the list of pending reports to delete and recreate an updated sequence for. 301 for (EventReport eventReport : sourceEventReports) { 302 // Delete pending reports since we may have different ones based on new trigger priority 303 // ordering. 304 if (eventReport.getReportTime() > triggerTime) { 305 reportsToDelete.add(eventReport); 306 continue; 307 } 308 309 UnsignedLong triggerData = eventReport.getTriggerData(); 310 311 // Event reports are sorted by summary bucket so this event report must be either for 312 // the first or the next bucket. The index for the map is one higher, corresponding to 313 // the current bucket we'll start with for attribution. 314 triggerDataToBucketIndexMap.merge(triggerData, 1, (oldValue, value) -> oldValue + 1); 315 316 List<Long> buckets = getSummaryBucketsForTriggerData(triggerData); 317 int bucketIndex = triggerDataToBucketIndexMap.get(triggerData) - 1; 318 long prevBucket = bucketIndex == 0 ? 0L : buckets.get(bucketIndex - 1); 319 long bucketSize = buckets.get(bucketIndex) - prevBucket; 320 321 for (AttributedTrigger attributedTrigger : mAttributedTriggersRef) { 322 bucketSize -= restoreTriggerContributionAndGetBucketDelta( 323 attributedTrigger, eventReport, bucketSize); 324 // We've covered the triggers that contributed to this report so we can exit the 325 // iteration. 326 if (bucketSize == 0L) { 327 break; 328 } 329 } 330 } 331 } 332 restoreTriggerContributionAndGetBucketDelta( AttributedTrigger attributedTrigger, EventReport eventReport, long bucketSize)333 private long restoreTriggerContributionAndGetBucketDelta( 334 AttributedTrigger attributedTrigger, EventReport eventReport, long bucketSize) { 335 // Skip this trigger since if it did not contribute to completed reports or if trigger data 336 // do not match. 337 if (attributedTrigger.getTriggerTime() >= eventReport.getReportTime() 338 || !Objects.equals(attributedTrigger.getTriggerData(), 339 eventReport.getTriggerData())) { 340 return 0L; 341 } 342 343 // Value sum operator. 344 if (getSummaryOperatorType(eventReport.getTriggerData()) 345 == TriggerSpec.SummaryOperatorType.VALUE_SUM) { 346 // The trigger can cover the full bucket size of the completed report. 347 if (attributedTrigger.remainingValue() >= bucketSize) { 348 attributedTrigger.addContribution(bucketSize); 349 return bucketSize; 350 // The trigger only covers some of the report's bucket. 351 } else { 352 long diff = attributedTrigger.remainingValue(); 353 attributedTrigger.addContribution(diff); 354 return diff; 355 } 356 // Count operator for a trigger that we haven't counted yet. 357 } else if (attributedTrigger.getContribution() == 0L) { 358 attributedTrigger.addContribution(1L); 359 return 1L; 360 } 361 362 return 0L; 363 } 364 buildPrivacyParameters(Source source, Flags flags)365 private void buildPrivacyParameters(Source source, Flags flags) { 366 mPrivacyParams = new PrivacyComputationParams(source, flags); 367 } 368 computePerTypeNumWindowList()369 private int[] computePerTypeNumWindowList() { 370 List<Integer> list = new ArrayList<>(); 371 for (TriggerSpec triggerSpec : mTriggerSpecs) { 372 for (UnsignedLong ignored : triggerSpec.getTriggerData()) { 373 list.add(triggerSpec.getEventReportWindowsEnd().size()); 374 } 375 } 376 return list.stream().mapToInt(Integer::intValue).toArray(); 377 } 378 computePerTypeCapList()379 private int[] computePerTypeCapList() { 380 List<Integer> list = new ArrayList<>(); 381 for (TriggerSpec triggerSpec : mTriggerSpecs) { 382 for (UnsignedLong ignored : triggerSpec.getTriggerData()) { 383 list.add(triggerSpec.getSummaryBuckets().size()); 384 } 385 } 386 return list.stream().mapToInt(Integer::intValue).toArray(); 387 } 388 389 @Override equals(Object obj)390 public boolean equals(Object obj) { 391 if (!(obj instanceof TriggerSpecs)) { 392 return false; 393 } 394 TriggerSpecs t = (TriggerSpecs) obj; 395 396 return mMaxEventLevelReports == t.mMaxEventLevelReports 397 && Objects.equals(mAttributedTriggersRef, t.mAttributedTriggersRef) 398 && Arrays.equals(mTriggerSpecs, t.mTriggerSpecs); 399 } 400 401 @Override hashCode()402 public int hashCode() { 403 return Objects.hash( 404 Arrays.hashCode(mTriggerSpecs), 405 mMaxEventLevelReports, 406 mAttributedTriggersRef); 407 } 408 409 /** 410 * Encode the privacy reporting parameters to JSON 411 * 412 * @return json object encode this class 413 */ encodeToJson()414 public String encodeToJson() { 415 return encodeToJson(mTriggerSpecs); 416 } 417 418 /** 419 * Encodes provided {@link TriggerSpec} into {@link JSONArray} string. 420 * 421 * @param triggerSpecs triggerSpec array to be encoded 422 * @return JSON encoded String 423 */ encodeToJson(TriggerSpec[] triggerSpecs)424 public static String encodeToJson(TriggerSpec[] triggerSpecs) { 425 try { 426 JSONObject[] triggerSpecsArray = new JSONObject[triggerSpecs.length]; 427 for (int i = 0; i < triggerSpecs.length; i++) { 428 triggerSpecsArray[i] = triggerSpecs[i].encodeJSON(); 429 } 430 return new JSONArray(triggerSpecsArray).toString(); 431 } catch (JSONException e) { 432 LoggerFactory.getMeasurementLogger() 433 .e("TriggerSpecs::encodeToJson is unable to encode TriggerSpecs"); 434 return null; 435 } 436 } 437 438 /** 439 * Encode the result of privacy parameters computed based on input parameters to JSON 440 * 441 * @return String encoded the privacy parameters 442 */ encodePrivacyParametersToJsonString()443 public String encodePrivacyParametersToJsonString() { 444 JSONObject json = new JSONObject(); 445 try { 446 json.put( 447 FlexEventReportJsonKeys.FLIP_PROBABILITY, 448 mPrivacyParams.mFlipProbability); 449 } catch (JSONException e) { 450 LoggerFactory.getMeasurementLogger() 451 .e( 452 "TriggerSpecs::encodePrivacyParametersToJsonString is unable to encode" 453 + " PrivacyParams to JSON"); 454 return null; 455 } 456 return json.toString(); 457 } 458 459 /** 460 * @param triggerData the triggerData to be checked 461 * @return whether the triggerData is registered 462 */ containsTriggerData(UnsignedLong triggerData)463 public boolean containsTriggerData(UnsignedLong triggerData) { 464 return mTriggerDataToTriggerSpecIndexMap.containsKey(triggerData); 465 } 466 467 /** 468 * @return the trigger data cardinality across all trigger specs 469 */ getTriggerDataCardinality()470 public int getTriggerDataCardinality() { 471 return mTriggerDataToTriggerSpecIndexMap.size(); 472 } 473 474 @VisibleForTesting getAttributedTriggers()475 public List<AttributedTrigger> getAttributedTriggers() { 476 return mAttributedTriggersRef; 477 } 478 479 private class PrivacyComputationParams { 480 private final int[] mPerTypeNumWindowList; 481 private final int[] mPerTypeCapList; 482 private long mNumStates; 483 private double mFlipProbability; 484 private double mInformationGain; 485 PrivacyComputationParams(Source source, Flags flags)486 PrivacyComputationParams(Source source, Flags flags) { 487 mPerTypeNumWindowList = computePerTypeNumWindowList(); 488 mPerTypeCapList = computePerTypeCapList(); 489 490 // Doubling the window cap for each trigger data type correlates with counting report 491 // states that treat having a web destination as different from an app destination. 492 int destinationMultiplier = source.getDestinationTypeMultiplier(flags); 493 int[] updatedPerTypeNumWindowList = new int[mPerTypeNumWindowList.length]; 494 for (int i = 0; i < mPerTypeNumWindowList.length; i++) { 495 updatedPerTypeNumWindowList[i] = mPerTypeNumWindowList[i] * destinationMultiplier; 496 } 497 498 long reportStateCountLimit = flags.getMeasurementMaxReportStatesPerSourceRegistration(); 499 500 long numStates = Combinatorics.getNumStatesFlexApi( 501 mMaxEventLevelReports, 502 updatedPerTypeNumWindowList, 503 mPerTypeCapList, 504 reportStateCountLimit); 505 506 if (numStates > reportStateCountLimit) { 507 return; 508 } 509 510 mNumStates = numStates; 511 mFlipProbability = 512 Combinatorics.getFlipProbability( 513 mNumStates, (double) flags.getMeasurementPrivacyEpsilon()); 514 mInformationGain = Combinatorics.getInformationGain(mNumStates, mFlipProbability); 515 } 516 PrivacyComputationParams(String inputLine)517 PrivacyComputationParams(String inputLine) throws JSONException { 518 JSONObject json = new JSONObject(inputLine); 519 mFlipProbability = 520 json.getDouble(FlexEventReportJsonKeys.FLIP_PROBABILITY); 521 mPerTypeNumWindowList = null; 522 mPerTypeCapList = null; 523 mNumStates = -1; 524 mInformationGain = -1.0; 525 } 526 hasValidReportStateCount()527 private boolean hasValidReportStateCount() { 528 return mNumStates != 0; 529 } 530 getNumStates()531 private long getNumStates() { 532 return mNumStates; 533 } 534 getFlipProbability()535 private double getFlipProbability() { 536 return mFlipProbability; 537 } 538 getInformationGain()539 private double getInformationGain() { 540 return mInformationGain; 541 } 542 getPerTypeNumWindowList()543 private int[] getPerTypeNumWindowList() { 544 return mPerTypeNumWindowList; 545 } 546 getPerTypeCapList()547 private int[] getPerTypeCapList() { 548 return mPerTypeCapList; 549 } 550 } 551 } 552