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