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