1 /* 2 * Copyright (C) 2022 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_DEFAULT; 20 import static android.adservices.customaudience.CustomAudience.PRIORITY_DEFAULT; 21 22 import android.adservices.common.AdSelectionSignals; 23 import android.adservices.common.AdTechIdentifier; 24 import android.adservices.common.ComponentAdData; 25 import android.adservices.customaudience.CustomAudience; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 30 import com.android.adservices.LoggerFactory; 31 import com.android.adservices.data.common.DBAdData; 32 import com.android.adservices.data.customaudience.DBTrustedBiddingData; 33 import com.android.adservices.service.Flags; 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.util.Preconditions; 36 37 import com.google.auto.value.AutoValue; 38 import com.google.common.collect.ImmutableList; 39 40 import org.json.JSONException; 41 import org.json.JSONObject; 42 43 import java.time.Instant; 44 import java.util.List; 45 import java.util.Objects; 46 47 /** This class represents the result of a daily fetch that will update a custom audience. */ 48 @AutoValue 49 public abstract class CustomAudienceUpdatableData { 50 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 51 52 @VisibleForTesting 53 enum ReadStatus { 54 STATUS_UNKNOWN, 55 STATUS_NOT_FOUND, 56 STATUS_FOUND_VALID, 57 STATUS_FOUND_INVALID 58 } 59 60 private static final String INVALID_JSON_TYPE_ERROR_FORMAT = 61 "%s Invalid JSON type while parsing %s found in JSON response"; 62 private static final String VALIDATION_FAILED_ERROR_FORMAT = 63 "%s Data validation failed while parsing %s found in JSON response"; 64 65 /** 66 * @return the user bidding signals that were sent in the update response. If there were no 67 * valid user bidding signals, returns {@code null}. 68 */ 69 @Nullable getUserBiddingSignals()70 public abstract AdSelectionSignals getUserBiddingSignals(); 71 72 /** 73 * @return trusted bidding data that was sent in the update response. If no valid trusted 74 * bidding data was found, returns {@code null}. 75 */ 76 @Nullable getTrustedBiddingData()77 public abstract DBTrustedBiddingData getTrustedBiddingData(); 78 79 /** 80 * @return the list of ads that were sent in the update response. If no valid ads were sent, 81 * returns {@code null}. 82 */ 83 @Nullable getAds()84 public abstract ImmutableList<DBAdData> getAds(); 85 86 /** @return the time at which the custom audience update was attempted */ 87 @NonNull getAttemptedUpdateTime()88 public abstract Instant getAttemptedUpdateTime(); 89 90 /** Returns the bitfield of auction server request flags. */ 91 @CustomAudience.AuctionServerRequestFlag getAuctionServerRequestFlags()92 public abstract int getAuctionServerRequestFlags(); 93 94 /** 95 * @return the list of component ads that were sent in the update response. If no valid ads were 96 * sent, returns {@code null}. 97 */ 98 @Nullable getComponentAds()99 public abstract List<ComponentAdData> getComponentAds(); 100 101 /** 102 * @return the priority value that was sent in the update response. If no value was sent, 103 * returns {@code 0.0}. 104 */ getPriority()105 public abstract double getPriority(); 106 107 /** 108 * @return the result type for the update attempt before {@link 109 * #createFromResponseString(Instant, AdTechIdentifier, 110 * BackgroundFetchRunner.UpdateResultType, String, Flags, boolean)} was called 111 */ getInitialUpdateResult()112 public abstract BackgroundFetchRunner.UpdateResultType getInitialUpdateResult(); 113 114 /** 115 * Returns whether this object represents a successful update. 116 * 117 * <ul> 118 * <li>An empty response is valid, representing that the buyer does not want to update its 119 * custom audience. 120 * <li>If a response is not empty but fails to be parsed into a JSON object, it will be 121 * considered a failed response which does not contain a successful update. 122 * <li>If a response is not empty and is parsed successfully into a JSON object but does not 123 * contain any units of updatable data, it is considered empty (albeit full of junk) and 124 * valid, representing that the buyer does not want to update its custom audience. 125 * <li>A non-empty response that contains relevant fields but which all fail to be parsed into 126 * valid objects is considered a failed update. This might happen if fields are found but 127 * do not follow the correct schema/expected object types. 128 * <li>A non-empty response that is not completely invalid and which does have at least one 129 * successful field is considered successful. 130 * </ul> 131 * 132 * @return {@code true} if this object represents a successful update; otherwise, {@code false} 133 */ getContainsSuccessfulUpdate()134 public abstract boolean getContainsSuccessfulUpdate(); 135 136 /** 137 * Creates a {@link CustomAudienceUpdatableData} object based on the response of a GET request 138 * to a custom audience's daily fetch URI. 139 * 140 * <p>Note that if a response contains extra fields in its JSON, the extra information will be 141 * ignored, and the validation of the response will continue as if the extra data had not been 142 * included. For example, if {@code trusted_bidding_data} contains an extra field {@code 143 * campaign_ids} (which is not considered part of the {@code trusted_bidding_data} JSON schema), 144 * the resulting {@link CustomAudienceUpdatableData} object will not be built with the extra 145 * data. 146 * 147 * <p>See {@link #getContainsSuccessfulUpdate()} for more details. 148 * 149 * @param attemptedUpdateTime the time at which the update for this custom audience was 150 * attempted 151 * @param buyer the buyer ad tech's eTLD+1 152 * @param initialUpdateResult the result type of the fetch attempt prior to parsing the {@code 153 * response} 154 * @param response the String response returned from querying the custom audience's daily fetch 155 * URI 156 * @param flags the {@link Flags} used to get configurable limits for validating the {@code 157 * response} 158 */ 159 @NonNull createFromResponseString( @onNull Instant attemptedUpdateTime, @NonNull AdTechIdentifier buyer, BackgroundFetchRunner.UpdateResultType initialUpdateResult, @NonNull final String response, @NonNull Flags flags)160 public static CustomAudienceUpdatableData createFromResponseString( 161 @NonNull Instant attemptedUpdateTime, 162 @NonNull AdTechIdentifier buyer, 163 BackgroundFetchRunner.UpdateResultType initialUpdateResult, 164 @NonNull final String response, 165 @NonNull Flags flags) { 166 Objects.requireNonNull(attemptedUpdateTime); 167 Objects.requireNonNull(buyer); 168 Objects.requireNonNull(response); 169 Objects.requireNonNull(flags); 170 171 // Use the hash of the response string as a session identifier for logging purposes 172 final String responseHash = "[" + response.hashCode() + "]"; 173 sLogger.v("Parsing JSON response string with hash %s", responseHash); 174 175 // By default unset nullable AutoValue fields are null 176 CustomAudienceUpdatableData.Builder dataBuilder = 177 builder() 178 .setAttemptedUpdateTime(attemptedUpdateTime) 179 .setContainsSuccessfulUpdate(false) 180 .setInitialUpdateResult(initialUpdateResult); 181 182 // No need to continue if an error occurred upstream for this custom audience update 183 if (initialUpdateResult != BackgroundFetchRunner.UpdateResultType.SUCCESS) { 184 sLogger.v("%s Skipping response string parsing due to upstream failure", responseHash); 185 dataBuilder.setContainsSuccessfulUpdate(false); 186 return dataBuilder.build(); 187 } 188 189 if (response.isEmpty()) { 190 sLogger.v("%s Response string was empty", responseHash); 191 dataBuilder.setContainsSuccessfulUpdate(true); 192 return dataBuilder.build(); 193 } 194 195 JSONObject responseObject; 196 try { 197 responseObject = new JSONObject(response); 198 } catch (JSONException exception) { 199 sLogger.e("%s Error parsing JSON response into an object", responseHash); 200 dataBuilder.setContainsSuccessfulUpdate(false); 201 return dataBuilder.build(); 202 } 203 204 CustomAudienceUpdatableDataReader reader = 205 new CustomAudienceUpdatableDataReader( 206 responseObject, 207 responseHash, 208 buyer, 209 flags.getFledgeCustomAudienceMaxUserBiddingSignalsSizeB(), 210 flags.getFledgeCustomAudienceMaxTrustedBiddingDataSizeB(), 211 flags.getFledgeCustomAudienceMaxAdsSizeB(), 212 flags.getFledgeCustomAudienceMaxNumAds(), 213 flags.getFledgeFrequencyCapFilteringEnabled(), 214 flags.getFledgeAppInstallFilteringEnabled(), 215 flags.getFledgeAuctionServerAdRenderIdEnabled(), 216 flags.getFledgeAuctionServerAdRenderIdMaxLength(), 217 flags.getComponentAdRenderIdMaxLengthBytes(), 218 flags.getMaxComponentAdsPerCustomAudience()); 219 220 ReadStatus userBiddingSignalsReadStatus = 221 readUserBiddingSignals(reader, responseHash, dataBuilder); 222 ReadStatus trustedBiddingDataReadStatus = 223 readTrustedBiddingData(reader, responseHash, dataBuilder); 224 ReadStatus adsReadStatus = readAds(reader, responseHash, dataBuilder); 225 226 ReadStatus auctionServerFlagStatus = ReadStatus.STATUS_UNKNOWN; 227 if (flags.getFledgeAuctionServerRequestFlagsEnabled()) { 228 auctionServerFlagStatus = 229 readAuctionServerRequestFlags(reader, responseHash, dataBuilder); 230 } 231 ReadStatus priorityValueStatus = ReadStatus.STATUS_UNKNOWN; 232 if (flags.getFledgeGetAdSelectionDataSellerConfigurationEnabled()) { 233 priorityValueStatus = readPriority(reader, responseHash, dataBuilder); 234 } 235 236 ReadStatus componentAdValueStatus = ReadStatus.STATUS_UNKNOWN; 237 if (flags.getEnableCustomAudienceComponentAds()) { 238 componentAdValueStatus = readComponentAds(reader, responseHash, dataBuilder); 239 } 240 241 // If there were no useful fields found, or if there was something useful found and 242 // successfully updated, then this object should signal a successful update. 243 boolean containsSuccessfulUpdate = 244 (userBiddingSignalsReadStatus == ReadStatus.STATUS_FOUND_VALID 245 || trustedBiddingDataReadStatus == ReadStatus.STATUS_FOUND_VALID 246 || adsReadStatus == ReadStatus.STATUS_FOUND_VALID) 247 || (userBiddingSignalsReadStatus == ReadStatus.STATUS_NOT_FOUND 248 && trustedBiddingDataReadStatus == ReadStatus.STATUS_NOT_FOUND 249 && adsReadStatus == ReadStatus.STATUS_NOT_FOUND) 250 || auctionServerFlagStatus == ReadStatus.STATUS_FOUND_VALID 251 || priorityValueStatus == ReadStatus.STATUS_FOUND_VALID 252 || componentAdValueStatus == ReadStatus.STATUS_FOUND_VALID; 253 sLogger.v( 254 "%s Completed parsing JSON response with containsSuccessfulUpdate = %b", 255 responseHash, containsSuccessfulUpdate); 256 dataBuilder.setContainsSuccessfulUpdate(containsSuccessfulUpdate); 257 258 return dataBuilder.build(); 259 } 260 261 @VisibleForTesting 262 @NonNull readUserBiddingSignals( @onNull CustomAudienceUpdatableDataReader reader, @NonNull String responseHash, @NonNull CustomAudienceUpdatableData.Builder dataBuilder)263 static ReadStatus readUserBiddingSignals( 264 @NonNull CustomAudienceUpdatableDataReader reader, 265 @NonNull String responseHash, 266 @NonNull CustomAudienceUpdatableData.Builder dataBuilder) { 267 try { 268 AdSelectionSignals userBiddingSignals = reader.getUserBiddingSignalsFromJsonObject(); 269 dataBuilder.setUserBiddingSignals(userBiddingSignals); 270 271 if (userBiddingSignals == null) { 272 return ReadStatus.STATUS_NOT_FOUND; 273 } else { 274 return ReadStatus.STATUS_FOUND_VALID; 275 } 276 } catch (JSONException | NullPointerException exception) { 277 sLogger.e( 278 exception, 279 INVALID_JSON_TYPE_ERROR_FORMAT, 280 responseHash, 281 CustomAudienceUpdatableDataReader.USER_BIDDING_SIGNALS_KEY); 282 dataBuilder.setUserBiddingSignals(null); 283 return ReadStatus.STATUS_FOUND_INVALID; 284 } catch (IllegalArgumentException exception) { 285 sLogger.e( 286 exception, 287 VALIDATION_FAILED_ERROR_FORMAT, 288 responseHash, 289 CustomAudienceUpdatableDataReader.USER_BIDDING_SIGNALS_KEY); 290 dataBuilder.setUserBiddingSignals(null); 291 return ReadStatus.STATUS_FOUND_INVALID; 292 } 293 } 294 295 @VisibleForTesting 296 @NonNull readTrustedBiddingData( @onNull CustomAudienceUpdatableDataReader reader, @NonNull String responseHash, @NonNull CustomAudienceUpdatableData.Builder dataBuilder)297 static ReadStatus readTrustedBiddingData( 298 @NonNull CustomAudienceUpdatableDataReader reader, 299 @NonNull String responseHash, 300 @NonNull CustomAudienceUpdatableData.Builder dataBuilder) { 301 try { 302 DBTrustedBiddingData trustedBiddingData = reader.getTrustedBiddingDataFromJsonObject(); 303 dataBuilder.setTrustedBiddingData(trustedBiddingData); 304 305 if (trustedBiddingData == null) { 306 return ReadStatus.STATUS_NOT_FOUND; 307 } else { 308 return ReadStatus.STATUS_FOUND_VALID; 309 } 310 } catch (JSONException | NullPointerException exception) { 311 sLogger.e( 312 exception, 313 INVALID_JSON_TYPE_ERROR_FORMAT, 314 responseHash, 315 CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_DATA_KEY); 316 dataBuilder.setTrustedBiddingData(null); 317 return ReadStatus.STATUS_FOUND_INVALID; 318 } catch (IllegalArgumentException exception) { 319 sLogger.e( 320 exception, 321 VALIDATION_FAILED_ERROR_FORMAT, 322 responseHash, 323 CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_DATA_KEY); 324 dataBuilder.setTrustedBiddingData(null); 325 return ReadStatus.STATUS_FOUND_INVALID; 326 } 327 } 328 329 @VisibleForTesting 330 @NonNull readAds( @onNull CustomAudienceUpdatableDataReader reader, @NonNull String responseHash, @NonNull CustomAudienceUpdatableData.Builder dataBuilder)331 static ReadStatus readAds( 332 @NonNull CustomAudienceUpdatableDataReader reader, 333 @NonNull String responseHash, 334 @NonNull CustomAudienceUpdatableData.Builder dataBuilder) { 335 try { 336 List<DBAdData> ads = reader.getAdsFromJsonObject(); 337 dataBuilder.setAds(ads); 338 339 if (ads == null) { 340 return ReadStatus.STATUS_NOT_FOUND; 341 } else { 342 return ReadStatus.STATUS_FOUND_VALID; 343 } 344 } catch (JSONException | NullPointerException exception) { 345 sLogger.e( 346 exception, 347 INVALID_JSON_TYPE_ERROR_FORMAT, 348 responseHash, 349 CustomAudienceUpdatableDataReader.ADS_KEY); 350 dataBuilder.setAds(null); 351 return ReadStatus.STATUS_FOUND_INVALID; 352 } catch (IllegalArgumentException exception) { 353 sLogger.e( 354 exception, 355 VALIDATION_FAILED_ERROR_FORMAT, 356 responseHash, 357 CustomAudienceUpdatableDataReader.ADS_KEY); 358 dataBuilder.setAds(null); 359 return ReadStatus.STATUS_FOUND_INVALID; 360 } 361 } 362 363 @VisibleForTesting 364 @NonNull readAuctionServerRequestFlags( @onNull CustomAudienceUpdatableDataReader reader, @NonNull String responseHash, @NonNull CustomAudienceUpdatableData.Builder dataBuilder)365 static ReadStatus readAuctionServerRequestFlags( 366 @NonNull CustomAudienceUpdatableDataReader reader, 367 @NonNull String responseHash, 368 @NonNull CustomAudienceUpdatableData.Builder dataBuilder) { 369 try { 370 dataBuilder.setAuctionServerRequestFlags(reader.getAuctionServerRequestFlags()); 371 return ReadStatus.STATUS_FOUND_VALID; 372 } catch (JSONException e) { 373 sLogger.e( 374 e, 375 INVALID_JSON_TYPE_ERROR_FORMAT, 376 responseHash, 377 CustomAudienceUpdatableDataReader.AUCTION_SERVER_REQUEST_FLAGS_KEY); 378 return ReadStatus.STATUS_FOUND_INVALID; 379 } 380 } 381 382 @VisibleForTesting 383 @NonNull readPriority( @onNull CustomAudienceUpdatableDataReader reader, @NonNull String responseHash, @NonNull CustomAudienceUpdatableData.Builder dataBuilder)384 static ReadStatus readPriority( 385 @NonNull CustomAudienceUpdatableDataReader reader, 386 @NonNull String responseHash, 387 @NonNull CustomAudienceUpdatableData.Builder dataBuilder) { 388 try { 389 dataBuilder.setPriority(reader.getPriority()); 390 return ReadStatus.STATUS_FOUND_VALID; 391 } catch (JSONException e) { 392 sLogger.e( 393 e, 394 INVALID_JSON_TYPE_ERROR_FORMAT, 395 responseHash, 396 CustomAudienceUpdatableDataReader.PRIORITY_KEY); 397 return ReadStatus.STATUS_FOUND_INVALID; 398 } 399 } 400 401 @VisibleForTesting readComponentAds( CustomAudienceUpdatableDataReader reader, String responseHash, CustomAudienceUpdatableData.Builder dataBuilder)402 static ReadStatus readComponentAds( 403 CustomAudienceUpdatableDataReader reader, 404 String responseHash, 405 CustomAudienceUpdatableData.Builder dataBuilder) { 406 try { 407 List<ComponentAdData> componentAdDataList = reader.getComponentAdsFromJsonObject(); 408 if (componentAdDataList == null) { 409 dataBuilder.setComponentAds(List.of()); 410 return ReadStatus.STATUS_NOT_FOUND; 411 } else { 412 dataBuilder.setComponentAds(componentAdDataList); 413 return ReadStatus.STATUS_FOUND_VALID; 414 } 415 } catch (JSONException | NullPointerException exception) { 416 sLogger.e( 417 exception, 418 INVALID_JSON_TYPE_ERROR_FORMAT, 419 responseHash, 420 CustomAudienceUpdatableDataReader.COMPONENT_ADS_KEY); 421 dataBuilder.setComponentAds(List.of()); 422 return ReadStatus.STATUS_FOUND_INVALID; 423 } catch (IllegalArgumentException exception) { 424 sLogger.e( 425 exception, 426 VALIDATION_FAILED_ERROR_FORMAT, 427 responseHash, 428 CustomAudienceUpdatableDataReader.COMPONENT_ADS_KEY); 429 dataBuilder.setComponentAds(List.of()); 430 return ReadStatus.STATUS_FOUND_INVALID; 431 } 432 } 433 434 /** 435 * Gets a Builder to make {@link #createFromResponseString(Instant, AdTechIdentifier, 436 * BackgroundFetchRunner.UpdateResultType, String, Flags, boolean)} easier. 437 */ 438 @VisibleForTesting 439 @NonNull builder()440 public static CustomAudienceUpdatableData.Builder builder() { 441 return new AutoValue_CustomAudienceUpdatableData.Builder() 442 .setAuctionServerRequestFlags(FLAG_AUCTION_SERVER_REQUEST_DEFAULT) 443 .setPriority(PRIORITY_DEFAULT) 444 .setComponentAds(List.of()); 445 } 446 447 /** 448 * This is a hidden (visible for testing) AutoValue builder to make {@link 449 * #createFromResponseString(Instant, AdTechIdentifier, BackgroundFetchRunner.UpdateResultType, 450 * String, Flags, boolean)} easier. 451 */ 452 @VisibleForTesting 453 @AutoValue.Builder 454 public abstract static class Builder { 455 /** Sets the user bidding signals found in the response string. */ 456 @NonNull setUserBiddingSignals(@ullable AdSelectionSignals value)457 public abstract Builder setUserBiddingSignals(@Nullable AdSelectionSignals value); 458 459 /** Sets the trusted bidding data found in the response string. */ 460 @NonNull setTrustedBiddingData(@ullable DBTrustedBiddingData value)461 public abstract Builder setTrustedBiddingData(@Nullable DBTrustedBiddingData value); 462 463 /** Sets the list of ads found in the response string. */ 464 @NonNull setAds(@ullable List<DBAdData> value)465 public abstract Builder setAds(@Nullable List<DBAdData> value); 466 467 /** Sets the time at which the custom audience update was attempted. */ 468 @NonNull setAttemptedUpdateTime(@onNull Instant value)469 public abstract Builder setAttemptedUpdateTime(@NonNull Instant value); 470 471 /** Sets the bitfield of auction server request flags. */ 472 @NonNull setAuctionServerRequestFlags( @ustomAudience.AuctionServerRequestFlag int auctionServerRequestFlags)473 public abstract Builder setAuctionServerRequestFlags( 474 @CustomAudience.AuctionServerRequestFlag int auctionServerRequestFlags); 475 476 /** Sets component ads. */ setComponentAds(@ullable List<ComponentAdData> componentAds)477 public abstract Builder setComponentAds(@Nullable List<ComponentAdData> componentAds); 478 479 /** Sets the priority value found in the response string. */ 480 @NonNull setPriority(double value)481 public abstract Builder setPriority(double value); 482 483 /** Sets the result of the update prior to parsing the response string. */ 484 @NonNull setInitialUpdateResult( BackgroundFetchRunner.UpdateResultType value)485 public abstract Builder setInitialUpdateResult( 486 BackgroundFetchRunner.UpdateResultType value); 487 488 /** 489 * Sets whether the response contained a successful update. 490 * 491 * <p>See {@link #getContainsSuccessfulUpdate()} for more details. 492 */ 493 @NonNull setContainsSuccessfulUpdate(boolean value)494 public abstract Builder setContainsSuccessfulUpdate(boolean value); 495 496 /** 497 * Builds the {@link CustomAudienceUpdatableData} object and returns it. 498 * 499 * <p>Note that AutoValue doesn't by itself do any validation, so splitting the builder with 500 * a manual verification is recommended. See go/autovalue/builders-howto#validate for more 501 * information. 502 */ 503 @NonNull autoValueBuild()504 protected abstract CustomAudienceUpdatableData autoValueBuild(); 505 506 /** Builds, validates, and returns the {@link CustomAudienceUpdatableData} object. */ 507 @NonNull build()508 public final CustomAudienceUpdatableData build() { 509 CustomAudienceUpdatableData updatableData = autoValueBuild(); 510 511 Preconditions.checkArgument( 512 updatableData.getContainsSuccessfulUpdate() 513 || (updatableData.getUserBiddingSignals() == null 514 && updatableData.getTrustedBiddingData() == null 515 && updatableData.getAds() == null 516 && updatableData.getComponentAds().isEmpty()), 517 "CustomAudienceUpdatableData should not contain non-null updatable fields if" 518 + " the object does not represent a successful update"); 519 520 return updatableData; 521 } 522 } 523 } 524