• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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