• 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 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