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