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