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.customaudience; 18 19 import static android.adservices.customaudience.CustomAudience.FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS; 20 import static android.adservices.customaudience.CustomAudience.PRIORITY_DEFAULT; 21 22 import static com.android.adservices.service.Flags.COMPONENT_AD_RENDER_ID_MAX_LENGTH_BYTES; 23 import static com.android.adservices.service.Flags.ENABLE_CUSTOM_AUDIENCE_COMPONENT_ADS; 24 import static com.android.adservices.service.Flags.FLEDGE_AUCTION_SERVER_AD_RENDER_ID_MAX_LENGTH; 25 import static com.android.adservices.service.Flags.MAX_COMPONENT_ADS_PER_CUSTOM_AUDIENCE; 26 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.ADS_KEY; 27 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.AD_COUNTERS_KEY; 28 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.AD_FILTERS_KEY; 29 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.AD_RENDER_ID_KEY; 30 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.COMPONENT_ADS_SIZE_EXCEEDS_MAX; 31 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.FIELD_FOUND_LOG_FORMAT; 32 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.FIELD_NOT_FOUND_LOG_FORMAT; 33 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.METADATA_KEY; 34 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.RENDER_URI_KEY; 35 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.SKIP_INVALID_JSON_TYPE_LOG_FORMAT; 36 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.STRING_ERROR_FORMAT; 37 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_DATA_KEY; 38 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_KEYS_KEY; 39 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_URI_KEY; 40 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.USER_BIDDING_SIGNALS_KEY; 41 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.ACTIVATION_TIME_KEY; 42 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.BIDDING_LOGIC_URI_KEY; 43 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.DAILY_UPDATE_URI_KEY; 44 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.EXPIRATION_TIME_KEY; 45 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.NAME_KEY; 46 47 import android.adservices.common.AdData; 48 import android.adservices.common.AdSelectionSignals; 49 import android.adservices.common.AdTechIdentifier; 50 import android.adservices.common.ComponentAdData; 51 import android.adservices.customaudience.CustomAudience; 52 import android.adservices.customaudience.FetchAndJoinCustomAudienceInput; 53 import android.adservices.customaudience.PartialCustomAudience; 54 import android.adservices.customaudience.TrustedBiddingData; 55 import android.net.Uri; 56 57 import com.android.adservices.LoggerFactory; 58 import com.android.adservices.data.common.DBAdData; 59 import com.android.adservices.data.customaudience.DBCustomAudience; 60 import com.android.adservices.data.customaudience.DBCustomAudienceBackgroundFetchData; 61 import com.android.adservices.service.common.AdRenderIdValidator; 62 import com.android.adservices.service.common.AdTechUriValidator; 63 import com.android.adservices.service.common.JsonUtils; 64 import com.android.adservices.service.common.ValidatorUtil; 65 import com.android.internal.annotations.VisibleForTesting; 66 67 import com.google.common.collect.Lists; 68 69 import org.json.JSONArray; 70 import org.json.JSONException; 71 import org.json.JSONObject; 72 73 import java.time.Instant; 74 import java.util.ArrayList; 75 import java.util.Arrays; 76 import java.util.LinkedHashMap; 77 import java.util.LinkedHashSet; 78 import java.util.List; 79 import java.util.Objects; 80 import java.util.Optional; 81 import java.util.function.BiFunction; 82 import java.util.function.Function; 83 84 /** 85 * Common representation of a custom audience. 86 * 87 * <p>A custom audience can be, partially or completely, represented in many ways: 88 * 89 * <ul> 90 * <li>{@link CustomAudience} or {@link FetchAndJoinCustomAudienceInput} as input from on-device 91 * callers. 92 * <li>{@link JSONObject} to/from a server. 93 * <li>{@link CustomAudienceUpdatableData} internally for daily fetch. 94 * <li>{@link DBCustomAudience} and {@link DBCustomAudienceBackgroundFetchData} to/from a DB. 95 * </ul> 96 * 97 * Each of the above are use-case specific and their fields have different properties. For example, 98 * a {@link CustomAudience#getAds()} may be malformed whereas {@link DBCustomAudience#getAds()} is 99 * validated to be well-formed. In contrast, {@link CustomAudienceBlob} is a generalized 100 * representation of a custom audience, that can be constructed to/from any of the above use-case 101 * specific representations to aid testing and development of features. 102 */ 103 public class CustomAudienceBlob { 104 // TODO(b/283857101): Remove the use functional interfaces and simplify by using individual 105 // named and typed fields instead. 106 /** 107 * Common representation of a custom audience's field. 108 * 109 * @param <T> the value of the field. 110 */ 111 static class Field<T> { 112 String mName; 113 T mValue; 114 Function<T, Object> mToJSONObject; 115 BiFunction<JSONObject, String, T> mFromJSONObject; 116 Field(Function<T, Object> toJSONObject, BiFunction<JSONObject, String, T> fromJSONObject)117 Field(Function<T, Object> toJSONObject, BiFunction<JSONObject, String, T> fromJSONObject) { 118 this.mToJSONObject = toJSONObject; 119 this.mFromJSONObject = fromJSONObject; 120 } 121 122 /** 123 * @return {@link JSONObject} representation of the {@link Field}. 124 */ toJSONObject()125 JSONObject toJSONObject() throws JSONException { 126 JSONObject json = new JSONObject(); 127 json.put(mName, mToJSONObject.apply(mValue)); 128 return json; 129 } 130 131 /** 132 * Populate the {@link Field#mName} and {@link Field#mValue} of the {@link Field} from its 133 * {@link JSONObject} representation. 134 */ fromJSONObject(JSONObject json, String key)135 void fromJSONObject(JSONObject json, String key) throws JSONException { 136 this.mName = key; 137 this.mValue = mFromJSONObject.apply(json, key); 138 } 139 } 140 141 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 142 public static final String OWNER_KEY = "owner"; 143 public static final String BUYER_KEY = "buyer"; 144 public static final String AUCTION_SERVER_REQUEST_FLAGS_KEY = "auction_server_request_flags"; 145 public static final String PRIORITY_KEY = "priority"; 146 public static final String OMIT_ADS_VALUE = "omit_ads"; 147 public static final String COMPONENT_ADS_KEY = "component_ads"; 148 static final LinkedHashSet<String> mKeysSet = 149 new LinkedHashSet<>( 150 Arrays.asList( 151 OWNER_KEY, 152 BUYER_KEY, 153 NAME_KEY, 154 ACTIVATION_TIME_KEY, 155 EXPIRATION_TIME_KEY, 156 DAILY_UPDATE_URI_KEY, 157 BIDDING_LOGIC_URI_KEY, 158 USER_BIDDING_SIGNALS_KEY, 159 TRUSTED_BIDDING_DATA_KEY, 160 ADS_KEY)); 161 final LinkedHashMap<String, Field<?>> mFieldsMap = new LinkedHashMap<>(); 162 private final ReadFiltersFromJsonStrategy mReadFiltersFromJsonStrategy; 163 private final ReadAdRenderIdFromJsonStrategy mReadAdRenderIdFromJsonStrategy; 164 private final boolean mAuctionServerRequestFlagsEnabled; 165 private final boolean mSellerConfigurationEnabled; 166 private final boolean mComponentAdsEnabled; 167 private final AdRenderIdValidator mComponentAdRenderIdValidator; 168 private final int mMaxNumComponentAds; 169 CustomAudienceBlob( boolean frequencyCapFilteringEnabled, boolean appInstallFilteringEnabled, boolean adRenderIdEnabled, long adRenderIdMaxLength, boolean auctionServerRequestFlagsEnabled, boolean sellerConfigurationEnabled, boolean componentAdsEnabled, int componentAdRenderIdMaxLength, int maxNumComponentAds)170 public CustomAudienceBlob( 171 boolean frequencyCapFilteringEnabled, 172 boolean appInstallFilteringEnabled, 173 boolean adRenderIdEnabled, 174 long adRenderIdMaxLength, 175 boolean auctionServerRequestFlagsEnabled, 176 boolean sellerConfigurationEnabled, 177 boolean componentAdsEnabled, 178 int componentAdRenderIdMaxLength, 179 int maxNumComponentAds) { 180 mReadFiltersFromJsonStrategy = 181 ReadFiltersFromJsonStrategyFactory.getStrategy( 182 frequencyCapFilteringEnabled, appInstallFilteringEnabled); 183 mReadAdRenderIdFromJsonStrategy = 184 ReadAdRenderIdFromJsonStrategyFactory.getStrategy( 185 adRenderIdEnabled, adRenderIdMaxLength); 186 mAuctionServerRequestFlagsEnabled = auctionServerRequestFlagsEnabled; 187 mSellerConfigurationEnabled = sellerConfigurationEnabled; 188 mComponentAdsEnabled = componentAdsEnabled; 189 mComponentAdRenderIdValidator = 190 AdRenderIdValidator.createEnabledInstance(componentAdRenderIdMaxLength); 191 mMaxNumComponentAds = maxNumComponentAds; 192 } 193 194 @VisibleForTesting CustomAudienceBlob()195 public CustomAudienceBlob() { 196 // TODO (b/356394210) Move this convenience method into a test fixture (or remove entirely) 197 // Filtering enabled by default. 198 this( 199 true, 200 true, 201 true, 202 FLEDGE_AUCTION_SERVER_AD_RENDER_ID_MAX_LENGTH, 203 false, 204 false, 205 ENABLE_CUSTOM_AUDIENCE_COMPONENT_ADS, 206 COMPONENT_AD_RENDER_ID_MAX_LENGTH_BYTES, 207 MAX_COMPONENT_ADS_PER_CUSTOM_AUDIENCE); 208 } 209 210 /** Update fields of the {@link CustomAudienceBlob} from a {@link JSONObject}. */ overrideFromJSONObject(JSONObject json)211 public void overrideFromJSONObject(JSONObject json) throws JSONException { 212 LinkedHashSet<String> jsonKeySet = new LinkedHashSet<>(Lists.newArrayList(json.keys())); 213 for (String key : mKeysSet) { 214 sLogger.v(key); 215 if (jsonKeySet.contains(key)) { 216 sLogger.v("Adding %s", key); 217 switch (key) { 218 case OWNER_KEY: 219 this.setOwner(this.getStringFromJSONObject(json, OWNER_KEY)); 220 break; 221 case BUYER_KEY: 222 this.setBuyer( 223 AdTechIdentifier.fromString( 224 this.getStringFromJSONObject(json, BUYER_KEY))); 225 break; 226 case NAME_KEY: 227 this.setName(this.getStringFromJSONObject(json, NAME_KEY)); 228 break; 229 case ACTIVATION_TIME_KEY: 230 this.setActivationTime( 231 this.getInstantFromJSONObject(json, ACTIVATION_TIME_KEY)); 232 break; 233 case EXPIRATION_TIME_KEY: 234 this.setExpirationTime( 235 this.getInstantFromJSONObject(json, EXPIRATION_TIME_KEY)); 236 break; 237 case DAILY_UPDATE_URI_KEY: 238 this.setDailyUpdateUri( 239 Uri.parse( 240 this.getStringFromJSONObject(json, DAILY_UPDATE_URI_KEY))); 241 break; 242 case BIDDING_LOGIC_URI_KEY: 243 this.setBiddingLogicUri( 244 Uri.parse( 245 this.getStringFromJSONObject(json, BIDDING_LOGIC_URI_KEY))); 246 break; 247 case USER_BIDDING_SIGNALS_KEY: 248 this.setUserBiddingSignals( 249 AdSelectionSignals.fromString( 250 json.getJSONObject(USER_BIDDING_SIGNALS_KEY).toString())); 251 break; 252 case TRUSTED_BIDDING_DATA_KEY: 253 this.setTrustedBiddingData( 254 this.getTrustedBiddingDataFromJSONObject( 255 json, TRUSTED_BIDDING_DATA_KEY)); 256 break; 257 case ADS_KEY: 258 this.setAds(this.getAdsFromJSONObject(json, ADS_KEY)); 259 } 260 } 261 } 262 // Set auction server flags if flag is enabled 263 if (mAuctionServerRequestFlagsEnabled 264 && jsonKeySet.contains(AUCTION_SERVER_REQUEST_FLAGS_KEY)) { 265 this.setAuctionServerRequestFlags( 266 this.getAuctionServerRequestFlagsFromJSONObject( 267 json, AUCTION_SERVER_REQUEST_FLAGS_KEY)); 268 } 269 270 // Set priority if seller configuration flag is enabled 271 if (mSellerConfigurationEnabled && jsonKeySet.contains(PRIORITY_KEY)) { 272 this.setPriority(this.getDoubleFromJSONObject(json, PRIORITY_KEY)); 273 } 274 275 if (mComponentAdsEnabled && jsonKeySet.contains(COMPONENT_ADS_KEY)) { 276 this.setComponentAds(this.getComponentAdsFromJSONObject(json, COMPONENT_ADS_KEY)); 277 } 278 } 279 280 /** 281 * Update fields of the {@link CustomAudienceBlob} from a {@link 282 * FetchAndJoinCustomAudienceInput}. 283 */ overrideFromFetchAndJoinCustomAudienceInput(FetchAndJoinCustomAudienceInput input)284 public void overrideFromFetchAndJoinCustomAudienceInput(FetchAndJoinCustomAudienceInput input) { 285 this.setOwner(input.getCallerPackageName()); 286 this.setBuyer(AdTechIdentifier.fromString(input.getFetchUri().getHost())); 287 288 if (input.getName() != null) { 289 this.setName(input.getName()); 290 } 291 if (input.getActivationTime() != null) { 292 this.setActivationTime(input.getActivationTime()); 293 } 294 if (input.getExpirationTime() != null) { 295 this.setExpirationTime(input.getExpirationTime()); 296 } 297 if (input.getUserBiddingSignals() != null) { 298 this.setUserBiddingSignals(input.getUserBiddingSignals()); 299 } 300 } 301 302 /** 303 * Utility methods to override a {@link CustomAudienceBlob} from a {@link PartialCustomAudience} 304 */ overrideFromPartialCustomAudience( String owner, AdTechIdentifier buyer, PartialCustomAudience partialCustomAudience)305 public void overrideFromPartialCustomAudience( 306 String owner, AdTechIdentifier buyer, PartialCustomAudience partialCustomAudience) { 307 this.setOwner(owner); 308 this.setBuyer(buyer); 309 310 this.setName(partialCustomAudience.getName()); 311 312 if (partialCustomAudience.getActivationTime() != null) { 313 this.setActivationTime(partialCustomAudience.getActivationTime()); 314 } 315 316 if (partialCustomAudience.getExpirationTime() != null) { 317 this.setExpirationTime(partialCustomAudience.getExpirationTime()); 318 } 319 320 if (partialCustomAudience.getUserBiddingSignals() != null) { 321 this.setUserBiddingSignals(partialCustomAudience.getUserBiddingSignals()); 322 } 323 } 324 325 /** 326 * @return {@link JSONObject} representation of the {@link CustomAudienceBlob}. 327 */ asJSONObject()328 public JSONObject asJSONObject() { 329 JSONObject json = new JSONObject(); 330 mFieldsMap.forEach( 331 (name, field) -> { 332 try { 333 json.put(name, field.toJSONObject().get(name)); 334 } catch (JSONException e) { 335 throw new RuntimeException(e); 336 } 337 }); 338 return json; 339 } 340 341 /** 342 * @return if the {@code owner} {@link Field} is set 343 */ hasOwner()344 public boolean hasOwner() { 345 return mFieldsMap.get(OWNER_KEY) != null; 346 } 347 348 /** 349 * @return the {@code owner} {@link Field} 350 */ getOwner()351 public String getOwner() { 352 return (String) mFieldsMap.get(OWNER_KEY).mValue; 353 } 354 355 /** set the {@code owner} {@link Field} */ setOwner(String value)356 public void setOwner(String value) { 357 if (mFieldsMap.containsKey(OWNER_KEY)) { 358 Field<String> field = (Field<String>) mFieldsMap.get(OWNER_KEY); 359 field.mValue = value; 360 } else { 361 Field<String> field = new Field<>((str) -> str, this::getStringFromJSONObject); 362 363 field.mName = OWNER_KEY; 364 field.mValue = value; 365 366 mFieldsMap.put(OWNER_KEY, field); 367 } 368 } 369 370 /** 371 * @return if the {@code buyer} {@link Field} is set 372 */ hasBuyer()373 public boolean hasBuyer() { 374 return mFieldsMap.get(BUYER_KEY) != null; 375 } 376 377 /** 378 * @return the {@code buyer} {@link Field} 379 */ getBuyer()380 public AdTechIdentifier getBuyer() { 381 return (AdTechIdentifier) mFieldsMap.get(BUYER_KEY).mValue; 382 } 383 384 /** set the {@code buyer} {@link Field} */ setBuyer(AdTechIdentifier value)385 public void setBuyer(AdTechIdentifier value) { 386 if (mFieldsMap.containsKey(BUYER_KEY)) { 387 Field<AdTechIdentifier> field = (Field<AdTechIdentifier>) mFieldsMap.get(BUYER_KEY); 388 field.mValue = value; 389 } else { 390 Field<AdTechIdentifier> field = 391 new Field<>( 392 Object::toString, 393 (json, key) -> 394 AdTechIdentifier.fromString( 395 this.getStringFromJSONObject(json, key))); 396 397 field.mName = BUYER_KEY; 398 field.mValue = value; 399 400 mFieldsMap.put(BUYER_KEY, field); 401 } 402 } 403 404 /** sets the {@code auctionServerRequestFlags} {@link Field} */ setAuctionServerRequestFlags(List<String> auctionServerRequestFlags)405 public void setAuctionServerRequestFlags(List<String> auctionServerRequestFlags) { 406 if (mFieldsMap.containsKey(AUCTION_SERVER_REQUEST_FLAGS_KEY)) { 407 Field<List<String>> field = 408 (Field<List<String>>) mFieldsMap.get(AUCTION_SERVER_REQUEST_FLAGS_KEY); 409 field.mValue = auctionServerRequestFlags; 410 } else { 411 Field<List<String>> field = 412 new Field<>( 413 this::getAuctionServerRequestFlagsAsJSONArray, 414 this::getAuctionServerRequestFlagsFromJSONObject); 415 416 field.mName = AUCTION_SERVER_REQUEST_FLAGS_KEY; 417 field.mValue = auctionServerRequestFlags; 418 419 mFieldsMap.put(AUCTION_SERVER_REQUEST_FLAGS_KEY, field); 420 } 421 } 422 423 /** Returns the server auction request bitfield extracted from the response, if found. */ 424 @CustomAudience.AuctionServerRequestFlag getAuctionServerRequestFlags()425 public int getAuctionServerRequestFlags() { 426 @CustomAudience.AuctionServerRequestFlag int result = 0; 427 if (mFieldsMap.containsKey(AUCTION_SERVER_REQUEST_FLAGS_KEY)) { 428 sLogger.d("Fields map contains auction server key"); 429 List<String> list = 430 (List<String>) mFieldsMap.get(AUCTION_SERVER_REQUEST_FLAGS_KEY).mValue; 431 for (String s : list) { 432 if (OMIT_ADS_VALUE.equals(s)) { 433 if ((result & FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS) == 0) { 434 // Only set the flag once 435 result = result | FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS; 436 } 437 } 438 } 439 } 440 return result; 441 } 442 443 /** 444 * @return if the {@code name} {@link Field} is set 445 */ hasName()446 public boolean hasName() { 447 return mFieldsMap.get(NAME_KEY) != null; 448 } 449 450 /** 451 * @return the {@code name} {@link Field} 452 */ getName()453 public String getName() { 454 return (String) mFieldsMap.get(NAME_KEY).mValue; 455 } 456 457 /** set the {@code name} {@link Field} */ setName(String value)458 public void setName(String value) { 459 if (mFieldsMap.containsKey(NAME_KEY)) { 460 Field<String> field = (Field<String>) mFieldsMap.get(NAME_KEY); 461 field.mValue = value; 462 } else { 463 Field<String> field = new Field<>((str) -> str, this::getStringFromJSONObject); 464 465 field.mName = NAME_KEY; 466 field.mValue = value; 467 468 mFieldsMap.put(NAME_KEY, field); 469 } 470 } 471 472 /** 473 * @return if the {@code activationTime} {@link Field} is set 474 */ hasActivationTime()475 public boolean hasActivationTime() { 476 return mFieldsMap.get(ACTIVATION_TIME_KEY) != null; 477 } 478 479 /** 480 * @return the {@code activationTime} {@link Field} 481 */ getActivationTime()482 public Instant getActivationTime() { 483 return (Instant) mFieldsMap.get(ACTIVATION_TIME_KEY).mValue; 484 } 485 486 /** set the {@code activationTime} {@link Field} */ setActivationTime(Instant value)487 public void setActivationTime(Instant value) { 488 if (mFieldsMap.containsKey(ACTIVATION_TIME_KEY)) { 489 Field<Instant> field = (Field<Instant>) mFieldsMap.get(ACTIVATION_TIME_KEY); 490 field.mValue = value; 491 } else { 492 Field<Instant> field = 493 new Field<>(Instant::toEpochMilli, this::getInstantFromJSONObject); 494 495 field.mName = ACTIVATION_TIME_KEY; 496 field.mValue = value; 497 498 mFieldsMap.put(ACTIVATION_TIME_KEY, field); 499 } 500 } 501 502 /** 503 * @return if the {@code expirationTime} {@link Field} is set 504 */ hasExpirationTime()505 public boolean hasExpirationTime() { 506 return mFieldsMap.get(EXPIRATION_TIME_KEY) != null; 507 } 508 509 /** 510 * @return the {@code expirationTime} {@link Field} 511 */ getExpirationTime()512 public Instant getExpirationTime() { 513 return (Instant) mFieldsMap.get(EXPIRATION_TIME_KEY).mValue; 514 } 515 516 /** set the {@code expirationTime} {@link Field} */ setExpirationTime(Instant value)517 public void setExpirationTime(Instant value) { 518 if (mFieldsMap.containsKey(EXPIRATION_TIME_KEY)) { 519 Field<Instant> field = (Field<Instant>) mFieldsMap.get(EXPIRATION_TIME_KEY); 520 field.mValue = value; 521 } else { 522 Field<Instant> field = 523 new Field<>(Instant::toEpochMilli, this::getInstantFromJSONObject); 524 525 field.mName = EXPIRATION_TIME_KEY; 526 field.mValue = value; 527 528 mFieldsMap.put(EXPIRATION_TIME_KEY, field); 529 } 530 } 531 532 /** 533 * @return if the {@code dailyUpdateUri} {@link Field} is set 534 */ hasDailyUpdateUri()535 public boolean hasDailyUpdateUri() { 536 return mFieldsMap.get(DAILY_UPDATE_URI_KEY) != null; 537 } 538 539 /** 540 * @return the {@code dailyUpdateUri} {@link Field} 541 */ getDailyUpdateUri()542 public Uri getDailyUpdateUri() { 543 return (Uri) mFieldsMap.get(DAILY_UPDATE_URI_KEY).mValue; 544 } 545 546 /** set the {@code dailyUpdateUri} {@link Field} */ setDailyUpdateUri(Uri value)547 public void setDailyUpdateUri(Uri value) { 548 if (mFieldsMap.containsKey(DAILY_UPDATE_URI_KEY)) { 549 Field<Uri> field = (Field<Uri>) mFieldsMap.get(DAILY_UPDATE_URI_KEY); 550 field.mValue = value; 551 } else { 552 Field<Uri> field = 553 new Field<>( 554 Uri::toString, 555 (json, key) -> Uri.parse(this.getStringFromJSONObject(json, key))); 556 557 field.mName = DAILY_UPDATE_URI_KEY; 558 field.mValue = value; 559 560 mFieldsMap.put(DAILY_UPDATE_URI_KEY, field); 561 } 562 } 563 564 /** 565 * @return if the {@code biddingLogicUri} {@link Field} is set 566 */ hasBiddingLogicUri()567 public boolean hasBiddingLogicUri() { 568 return mFieldsMap.get(BIDDING_LOGIC_URI_KEY) != null; 569 } 570 571 /** 572 * @return the {@code biddingLogicUri} {@link Field} 573 */ getBiddingLogicUri()574 public Uri getBiddingLogicUri() { 575 return (Uri) mFieldsMap.get(BIDDING_LOGIC_URI_KEY).mValue; 576 } 577 578 /** set the {@code biddingLogicUri} {@link Field} */ setBiddingLogicUri(Uri value)579 public void setBiddingLogicUri(Uri value) { 580 if (mFieldsMap.containsKey(BIDDING_LOGIC_URI_KEY)) { 581 Field<Uri> field = (Field<Uri>) mFieldsMap.get(BIDDING_LOGIC_URI_KEY); 582 field.mValue = value; 583 } else { 584 Field<Uri> field = 585 new Field<>( 586 Uri::toString, 587 (json, key) -> Uri.parse(this.getStringFromJSONObject(json, key))); 588 589 field.mName = BIDDING_LOGIC_URI_KEY; 590 field.mValue = value; 591 592 mFieldsMap.put(BIDDING_LOGIC_URI_KEY, field); 593 } 594 } 595 596 /** 597 * @return if the {@code userBiddingSignals} {@link Field} is set 598 */ hasUserBiddingSignals()599 public boolean hasUserBiddingSignals() { 600 return mFieldsMap.get(USER_BIDDING_SIGNALS_KEY) != null; 601 } 602 603 /** 604 * @return the {@code userBiddingSignals} {@link Field} 605 */ getUserBiddingSignals()606 public AdSelectionSignals getUserBiddingSignals() { 607 return (AdSelectionSignals) mFieldsMap.get(USER_BIDDING_SIGNALS_KEY).mValue; 608 } 609 610 /** set the {@code userBiddingSignals} {@link Field} */ setUserBiddingSignals(AdSelectionSignals value)611 public void setUserBiddingSignals(AdSelectionSignals value) { 612 if (mFieldsMap.containsKey(USER_BIDDING_SIGNALS_KEY)) { 613 Field<AdSelectionSignals> field = 614 (Field<AdSelectionSignals>) mFieldsMap.get(USER_BIDDING_SIGNALS_KEY); 615 field.mValue = value; 616 } else { 617 Field<AdSelectionSignals> field = 618 new Field<>( 619 (adSelectionSignals) -> { 620 try { 621 return new JSONObject(adSelectionSignals.toString()); 622 } catch (JSONException e) { 623 throw new RuntimeException(e); 624 } 625 }, 626 (json, key) -> { 627 try { 628 return AdSelectionSignals.fromString( 629 json.getJSONObject(key).toString()); 630 } catch (JSONException e) { 631 throw new RuntimeException(e); 632 } 633 }); 634 635 field.mName = USER_BIDDING_SIGNALS_KEY; 636 field.mValue = value; 637 638 mFieldsMap.put(USER_BIDDING_SIGNALS_KEY, field); 639 } 640 } 641 642 /** 643 * @return if the {@code trustedBiddingData} {@link Field} is set 644 */ hasTrustedBiddingData()645 public boolean hasTrustedBiddingData() { 646 return mFieldsMap.get(TRUSTED_BIDDING_DATA_KEY) != null; 647 } 648 649 /** 650 * @return the {@code trustedBiddingData} {@link Field} 651 */ getTrustedBiddingData()652 public TrustedBiddingData getTrustedBiddingData() { 653 return (TrustedBiddingData) mFieldsMap.get(TRUSTED_BIDDING_DATA_KEY).mValue; 654 } 655 656 /** set the {@code trustedBiddingData} {@link Field} */ setTrustedBiddingData(TrustedBiddingData value)657 public void setTrustedBiddingData(TrustedBiddingData value) { 658 if (mFieldsMap.containsKey(TRUSTED_BIDDING_DATA_KEY)) { 659 Field<TrustedBiddingData> field = 660 (Field<TrustedBiddingData>) mFieldsMap.get(TRUSTED_BIDDING_DATA_KEY); 661 field.mValue = value; 662 } else { 663 Field<TrustedBiddingData> field = 664 new Field<>( 665 this::getTrustedBiddingDataAsJSONObject, 666 this::getTrustedBiddingDataFromJSONObject); 667 668 field.mName = TRUSTED_BIDDING_DATA_KEY; 669 field.mValue = value; 670 671 mFieldsMap.put(TRUSTED_BIDDING_DATA_KEY, field); 672 } 673 } 674 getTrustedBiddingDataAsJSONObject(TrustedBiddingData value)675 private JSONObject getTrustedBiddingDataAsJSONObject(TrustedBiddingData value) { 676 try { 677 JSONObject json = new JSONObject(); 678 json.put(TRUSTED_BIDDING_URI_KEY, value.getTrustedBiddingUri().toString()); 679 json.put(TRUSTED_BIDDING_KEYS_KEY, new JSONArray(value.getTrustedBiddingKeys())); 680 return json; 681 } catch (JSONException e) { 682 throw new RuntimeException(e); 683 } 684 } 685 getTrustedBiddingDataFromJSONObject(JSONObject json, String key)686 private TrustedBiddingData getTrustedBiddingDataFromJSONObject(JSONObject json, String key) { 687 return getValueFromJSONObject( 688 json, 689 key, 690 (jsonObject, jsonKey) -> { 691 try { 692 JSONObject dataJsonObj = jsonObject.getJSONObject(jsonKey); 693 694 String uri = 695 JsonUtils.getStringFromJson( 696 dataJsonObj, 697 TRUSTED_BIDDING_URI_KEY, 698 String.format( 699 STRING_ERROR_FORMAT, TRUSTED_BIDDING_URI_KEY, key)); 700 Uri parsedUri = Uri.parse(uri); 701 702 JSONArray keysJsonArray = 703 dataJsonObj.getJSONArray(TRUSTED_BIDDING_KEYS_KEY); 704 int keysListLength = keysJsonArray.length(); 705 List<String> keysList = new ArrayList<>(keysListLength); 706 for (int i = 0; i < keysListLength; i++) { 707 try { 708 keysList.add( 709 JsonUtils.getStringFromJsonArrayAtIndex( 710 keysJsonArray, 711 i, 712 String.format( 713 STRING_ERROR_FORMAT, 714 TRUSTED_BIDDING_KEYS_KEY, 715 key))); 716 } catch (JSONException | NullPointerException exception) { 717 // Skip any keys that are malformed and continue to the next in the 718 // list; note that if the entire given list of keys is junk, then 719 // any existing trusted bidding keys are cleared from the custom 720 // audience 721 sLogger.v( 722 SKIP_INVALID_JSON_TYPE_LOG_FORMAT, 723 json.hashCode(), 724 TRUSTED_BIDDING_KEYS_KEY, 725 Optional.ofNullable(exception.getMessage()) 726 .orElse("<null>")); 727 } 728 } 729 730 TrustedBiddingData trustedBiddingData = 731 new TrustedBiddingData.Builder() 732 .setTrustedBiddingUri(parsedUri) 733 .setTrustedBiddingKeys(keysList) 734 .build(); 735 736 return trustedBiddingData; 737 } catch (JSONException e) { 738 throw new RuntimeException(e); 739 } 740 }); 741 } 742 743 /** 744 * @return if the {@code ads} {@link Field} is set 745 */ 746 public boolean hasAds() { 747 return mFieldsMap.get(ADS_KEY) != null; 748 } 749 750 /** 751 * @return the {@code ads} {@link Field} 752 */ 753 public List<AdData> getAds() { 754 return (List<AdData>) mFieldsMap.get(ADS_KEY).mValue; 755 } 756 757 /** set the {@code ads} {@link Field} */ 758 public void setAds(List<AdData> value) { 759 if (mFieldsMap.containsKey(ADS_KEY)) { 760 Field<List<AdData>> field = (Field<List<AdData>>) mFieldsMap.get(ADS_KEY); 761 field.mValue = value; 762 } else { 763 Field<List<AdData>> field = 764 new Field<>(this::getAdsAsJSONObject, this::getAdsFromJSONObject); 765 766 field.mName = ADS_KEY; 767 field.mValue = value; 768 769 mFieldsMap.put(ADS_KEY, field); 770 } 771 } 772 773 /** 774 * @return the {@code priority} {@link Field} 775 */ 776 public double getPriority() { 777 double result = PRIORITY_DEFAULT; 778 if (mFieldsMap.containsKey(PRIORITY_KEY)) { 779 result = (double) mFieldsMap.get(PRIORITY_KEY).mValue; 780 } 781 return result; 782 } 783 784 /** set the {@code priority} {@link Field} */ 785 public void setPriority(double value) { 786 if (mFieldsMap.containsKey(PRIORITY_KEY)) { 787 Field<Double> field = (Field<Double>) mFieldsMap.get(PRIORITY_KEY); 788 field.mValue = value; 789 } else { 790 Field<Double> field = new Field<>((dbl) -> dbl, this::getDoubleFromJSONObject); 791 792 field.mName = PRIORITY_KEY; 793 field.mValue = value; 794 795 mFieldsMap.put(PRIORITY_KEY, field); 796 } 797 } 798 799 /** set the {@code componentAds} {@link Field} */ 800 public void setComponentAds(List<ComponentAdData> value) { 801 if (mFieldsMap.containsKey(COMPONENT_ADS_KEY)) { 802 Field<List<ComponentAdData>> field = 803 (Field<List<ComponentAdData>>) mFieldsMap.get(COMPONENT_ADS_KEY); 804 field.mValue = value; 805 } else { 806 Field<List<ComponentAdData>> field = 807 new Field<>( 808 this::getComponentAdsAdsAsJSONObject, 809 this::getComponentAdsFromJSONObject); 810 811 field.mName = COMPONENT_ADS_KEY; 812 field.mValue = value; 813 814 mFieldsMap.put(COMPONENT_ADS_KEY, field); 815 } 816 } 817 818 /** 819 * @return the {@code componentAds} {@link Field} 820 */ 821 public List<ComponentAdData> getComponentAds() { 822 if (mFieldsMap.containsKey(COMPONENT_ADS_KEY)) { 823 return (List<ComponentAdData>) mFieldsMap.get(COMPONENT_ADS_KEY).mValue; 824 } 825 return List.of(); 826 } 827 828 private JSONArray getAdsAsJSONObject(List<AdData> value) { 829 try { 830 JSONArray adsJson = new JSONArray(); 831 for (AdData ad : value) { 832 JSONObject adJson = new JSONObject(); 833 834 adJson.put(RENDER_URI_KEY, ad.getRenderUri().toString()); 835 try { 836 adJson.put(METADATA_KEY, new JSONObject(ad.getMetadata())); 837 } catch (JSONException exception) { 838 sLogger.v( 839 "Trying to add invalid JSON to test object (%s); inserting as String" 840 + " instead", 841 exception.getMessage()); 842 adJson.put(METADATA_KEY, ad.getMetadata()); 843 } 844 if (!ad.getAdCounterKeys().isEmpty()) { 845 adJson.put(AD_COUNTERS_KEY, new JSONArray(ad.getAdCounterKeys())); 846 } 847 if (ad.getAdFilters() != null) { 848 adJson.put(AD_FILTERS_KEY, ad.getAdFilters().toJson()); 849 } 850 if (ad.getAdRenderId() != null) { 851 adJson.put(AD_RENDER_ID_KEY, ad.getAdRenderId()); 852 } 853 adsJson.put(adJson); 854 } 855 return adsJson; 856 } catch (JSONException e) { 857 throw new RuntimeException(e); 858 } 859 } 860 861 private JSONArray getComponentAdsAdsAsJSONObject(List<ComponentAdData> componentAds) { 862 try { 863 JSONArray componentAdsJson = new JSONArray(); 864 for (ComponentAdData componentAd : componentAds) { 865 JSONObject componentAdJson = new JSONObject(); 866 867 componentAdJson.put(RENDER_URI_KEY, componentAd.getRenderUri().toString()); 868 componentAdJson.put(AD_RENDER_ID_KEY, componentAd.getAdRenderId()); 869 870 componentAdsJson.put(componentAdJson); 871 } 872 return componentAdsJson; 873 } catch (JSONException e) { 874 // Don't want to throw an exception since this is an optional field 875 return new JSONArray(); 876 } 877 } 878 879 private JSONArray getAuctionServerRequestFlagsAsJSONArray(List<String> value) { 880 return new JSONArray(value); 881 } 882 883 @SuppressWarnings("FormatStringAnnotation") 884 private List<String> getAuctionServerRequestFlagsFromJSONObject(JSONObject json, String key) { 885 return getValueFromJSONObject( 886 json, 887 key, 888 (jsonObject, jsonKey) -> { 889 List<String> resultList = new ArrayList<>(); 890 try { 891 JSONArray flagsJsonArray = jsonObject.getJSONArray(key); 892 for (int i = 0; i < flagsJsonArray.length(); i++) { 893 try { 894 resultList.add( 895 JsonUtils.getStringFromJsonArrayAtIndex( 896 flagsJsonArray, 897 i, 898 SKIP_INVALID_JSON_TYPE_LOG_FORMAT)); 899 } catch (JSONException e) { 900 sLogger.v( 901 SKIP_INVALID_JSON_TYPE_LOG_FORMAT, 902 jsonObject.hashCode(), 903 key, 904 Optional.ofNullable(e.getMessage()).orElse("<null>")); 905 } 906 } 907 } catch (JSONException e) { 908 // Ignore since we don't want to fail if there is an issue with this 909 // optional field 910 sLogger.v( 911 FIELD_NOT_FOUND_LOG_FORMAT, 912 jsonObject.hashCode(), 913 key, 914 Optional.ofNullable(e.getMessage()).orElse("<null>")); 915 } 916 return resultList; 917 }); 918 } 919 920 private List<AdData> getAdsFromJSONObject(JSONObject json, String key) { 921 return getValueFromJSONObject( 922 json, 923 key, 924 (jsonObject, jsonKey) -> { 925 try { 926 JSONArray adsJsonArray = jsonObject.getJSONArray(key); 927 int adsListLength = adsJsonArray.length(); 928 List<AdData> adsList = new ArrayList<>(); 929 for (int i = 0; i < adsListLength; i++) { 930 JSONObject adDataJsonObj = adsJsonArray.getJSONObject(i); 931 932 // Note: getString() coerces values to be strings; use get() instead 933 Object uri = adDataJsonObj.get(RENDER_URI_KEY); 934 if (!(uri instanceof String)) { 935 throw new JSONException( 936 "Unexpected format parsing " 937 + RENDER_URI_KEY 938 + " in " 939 + key); 940 } 941 Uri parsedUri = Uri.parse(Objects.requireNonNull((String) uri)); 942 943 String metadata = 944 Objects.requireNonNull( 945 adDataJsonObj.getJSONObject(METADATA_KEY)) 946 .toString(); 947 948 DBAdData.Builder adDataBuilder = 949 new DBAdData.Builder() 950 .setRenderUri(parsedUri) 951 .setMetadata(metadata); 952 953 mReadFiltersFromJsonStrategy.readFilters(adDataBuilder, adDataJsonObj); 954 mReadAdRenderIdFromJsonStrategy.readId(adDataBuilder, adDataJsonObj); 955 956 DBAdData dbAdData = adDataBuilder.build(); 957 AdData adData = 958 new AdData.Builder() 959 .setMetadata(dbAdData.getMetadata()) 960 .setRenderUri(dbAdData.getRenderUri()) 961 .setAdCounterKeys(dbAdData.getAdCounterKeys()) 962 .setAdFilters(dbAdData.getAdFilters()) 963 .setAdRenderId(dbAdData.getAdRenderId()) 964 .build(); 965 966 adsList.add(adData); 967 } 968 return adsList; 969 } catch (JSONException e) { 970 throw new RuntimeException(e); 971 } 972 }); 973 } 974 975 private List<ComponentAdData> getComponentAdsFromJSONObject(JSONObject json, String key) { 976 return getValueFromJSONObject( 977 json, 978 key, 979 (jsonObject, jsonKey) -> { 980 try { 981 AdTechIdentifier buyer = 982 AdTechIdentifier.fromString( 983 this.getStringFromJSONObject(json, BUYER_KEY)); 984 985 JSONArray componentAdsJsonArray = jsonObject.getJSONArray(key); 986 987 int componentAdsListLength = componentAdsJsonArray.length(); 988 // TODO(b/381392728):investigate whether we need an overall size on list of 989 // component 990 if (componentAdsListLength > mMaxNumComponentAds) { 991 sLogger.v(COMPONENT_ADS_SIZE_EXCEEDS_MAX); 992 throw new IllegalArgumentException(COMPONENT_ADS_SIZE_EXCEEDS_MAX); 993 } 994 List<ComponentAdData> componentAdsList = new ArrayList<>(); 995 for (int i = 0; i < componentAdsListLength; i++) { 996 try { 997 JSONObject componentAdDataJsonObj = 998 componentAdsJsonArray.getJSONObject(i); 999 1000 // Note: getString() coerces values to be strings; use get() instead 1001 Object uri = componentAdDataJsonObj.get(RENDER_URI_KEY); 1002 if (!(uri instanceof String)) { 1003 throw new JSONException( 1004 "Unexpected format parsing " 1005 + RENDER_URI_KEY 1006 + " in " 1007 + COMPONENT_ADS_KEY); 1008 } 1009 Uri parsedUri = Uri.parse(Objects.requireNonNull((String) uri)); 1010 1011 AdTechUriValidator uriValidator = 1012 new AdTechUriValidator( 1013 ValidatorUtil.AD_TECH_ROLE_BUYER, 1014 buyer.toString(), 1015 this.getClass().getSimpleName(), 1016 RENDER_URI_KEY); 1017 uriValidator.validate(parsedUri); 1018 1019 Object adRenderId = componentAdDataJsonObj.get(AD_RENDER_ID_KEY); 1020 if (!(adRenderId instanceof String adRenderIdString)) { 1021 throw new JSONException( 1022 "Unexpected format parsing " 1023 + AD_RENDER_ID_KEY 1024 + " in " 1025 + COMPONENT_ADS_KEY); 1026 } 1027 mComponentAdRenderIdValidator.validate(adRenderIdString); 1028 1029 ComponentAdData componentAdData = 1030 new ComponentAdData(parsedUri, adRenderIdString); 1031 componentAdsList.add(componentAdData); 1032 } catch (JSONException 1033 | NullPointerException 1034 | IllegalArgumentException exception) { 1035 // We don't want to fail since this is an optional field, 1036 return new ArrayList<>(); 1037 } 1038 } 1039 return componentAdsList; 1040 } catch (JSONException | IllegalArgumentException e) { 1041 // Ignore since we don't want to fail if there is an issue with this 1042 // optional field 1043 return new ArrayList<>(); 1044 } 1045 }); 1046 } 1047 1048 private String getStringFromJSONObject(JSONObject json, String key) { 1049 return getValueFromJSONObject( 1050 json, 1051 key, 1052 (jsonObject, jsonKey) -> { 1053 try { 1054 return JsonUtils.getStringFromJson( 1055 json, 1056 key, 1057 String.format(STRING_ERROR_FORMAT, key, json.hashCode())); 1058 } catch (JSONException e) { 1059 throw new RuntimeException(e); 1060 } 1061 }); 1062 } 1063 1064 private Double getDoubleFromJSONObject(JSONObject json, String key) { 1065 return getValueFromJSONObject( 1066 json, 1067 key, 1068 (jsonObject, jsonKey) -> { 1069 double priority = PRIORITY_DEFAULT; 1070 try { 1071 priority = jsonObject.getDouble(jsonKey); 1072 } catch (JSONException e) { 1073 // Ignore since we don't want to fail if there is an issue with this 1074 // optional field 1075 sLogger.d(String.valueOf(e)); 1076 } 1077 return priority; 1078 }); 1079 } 1080 1081 private Instant getInstantFromJSONObject(JSONObject json, String key) { 1082 return getValueFromJSONObject( 1083 json, 1084 key, 1085 (jsonObject, jsonKey) -> { 1086 try { 1087 return Instant.ofEpochMilli(jsonObject.getLong(jsonKey)); 1088 } catch (Exception e) { 1089 throw new RuntimeException(e); 1090 } 1091 }); 1092 } 1093 1094 private <T> T getValueFromJSONObject( 1095 JSONObject json, String key, BiFunction<JSONObject, String, T> fromJsonObject) { 1096 if (json.has(key)) { 1097 sLogger.v(FIELD_FOUND_LOG_FORMAT, json.hashCode(), key); 1098 return fromJsonObject.apply(json, key); 1099 } else { 1100 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, json.hashCode(), ACTIVATION_TIME_KEY); 1101 return null; 1102 } 1103 } 1104 } 1105