• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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_OMIT_ADS;
20 import static android.adservices.customaudience.CustomAudience.PRIORITY_DEFAULT;
21 
22 import static com.android.adservices.service.Flags.COMPONENT_AD_RENDER_ID_MAX_LENGTH_BYTES;
23 import static com.android.adservices.service.Flags.ENABLE_CUSTOM_AUDIENCE_COMPONENT_ADS;
24 import static com.android.adservices.service.Flags.FLEDGE_AUCTION_SERVER_AD_RENDER_ID_MAX_LENGTH;
25 import static com.android.adservices.service.Flags.MAX_COMPONENT_ADS_PER_CUSTOM_AUDIENCE;
26 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.ADS_KEY;
27 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.AD_COUNTERS_KEY;
28 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.AD_FILTERS_KEY;
29 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.AD_RENDER_ID_KEY;
30 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.COMPONENT_ADS_SIZE_EXCEEDS_MAX;
31 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.FIELD_FOUND_LOG_FORMAT;
32 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.FIELD_NOT_FOUND_LOG_FORMAT;
33 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.METADATA_KEY;
34 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.RENDER_URI_KEY;
35 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.SKIP_INVALID_JSON_TYPE_LOG_FORMAT;
36 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.STRING_ERROR_FORMAT;
37 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_DATA_KEY;
38 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_KEYS_KEY;
39 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_URI_KEY;
40 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.USER_BIDDING_SIGNALS_KEY;
41 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.ACTIVATION_TIME_KEY;
42 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.BIDDING_LOGIC_URI_KEY;
43 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.DAILY_UPDATE_URI_KEY;
44 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.EXPIRATION_TIME_KEY;
45 import static com.android.adservices.service.customaudience.FetchCustomAudienceReader.NAME_KEY;
46 
47 import android.adservices.common.AdData;
48 import android.adservices.common.AdSelectionSignals;
49 import android.adservices.common.AdTechIdentifier;
50 import android.adservices.common.ComponentAdData;
51 import android.adservices.customaudience.CustomAudience;
52 import android.adservices.customaudience.FetchAndJoinCustomAudienceInput;
53 import android.adservices.customaudience.PartialCustomAudience;
54 import android.adservices.customaudience.TrustedBiddingData;
55 import android.net.Uri;
56 
57 import com.android.adservices.LoggerFactory;
58 import com.android.adservices.data.common.DBAdData;
59 import com.android.adservices.data.customaudience.DBCustomAudience;
60 import com.android.adservices.data.customaudience.DBCustomAudienceBackgroundFetchData;
61 import com.android.adservices.service.common.AdRenderIdValidator;
62 import com.android.adservices.service.common.AdTechUriValidator;
63 import com.android.adservices.service.common.JsonUtils;
64 import com.android.adservices.service.common.ValidatorUtil;
65 import com.android.internal.annotations.VisibleForTesting;
66 
67 import com.google.common.collect.Lists;
68 
69 import org.json.JSONArray;
70 import org.json.JSONException;
71 import org.json.JSONObject;
72 
73 import java.time.Instant;
74 import java.util.ArrayList;
75 import java.util.Arrays;
76 import java.util.LinkedHashMap;
77 import java.util.LinkedHashSet;
78 import java.util.List;
79 import java.util.Objects;
80 import java.util.Optional;
81 import java.util.function.BiFunction;
82 import java.util.function.Function;
83 
84 /**
85  * Common representation of a custom audience.
86  *
87  * <p>A custom audience can be, partially or completely, represented in many ways:
88  *
89  * <ul>
90  *   <li>{@link CustomAudience} or {@link FetchAndJoinCustomAudienceInput} as input from on-device
91  *       callers.
92  *   <li>{@link JSONObject} to/from a server.
93  *   <li>{@link CustomAudienceUpdatableData} internally for daily fetch.
94  *   <li>{@link DBCustomAudience} and {@link DBCustomAudienceBackgroundFetchData} to/from a DB.
95  * </ul>
96  *
97  * Each of the above are use-case specific and their fields have different properties. For example,
98  * a {@link CustomAudience#getAds()} may be malformed whereas {@link DBCustomAudience#getAds()} is
99  * validated to be well-formed. In contrast, {@link CustomAudienceBlob} is a generalized
100  * representation of a custom audience, that can be constructed to/from any of the above use-case
101  * specific representations to aid testing and development of features.
102  */
103 public class CustomAudienceBlob {
104     // TODO(b/283857101): Remove the use functional interfaces and simplify by using individual
105     //  named and typed fields instead.
106     /**
107      * Common representation of a custom audience's field.
108      *
109      * @param <T> the value of the field.
110      */
111     static class Field<T> {
112         String mName;
113         T mValue;
114         Function<T, Object> mToJSONObject;
115         BiFunction<JSONObject, String, T> mFromJSONObject;
116 
Field(Function<T, Object> toJSONObject, BiFunction<JSONObject, String, T> fromJSONObject)117         Field(Function<T, Object> toJSONObject, BiFunction<JSONObject, String, T> fromJSONObject) {
118             this.mToJSONObject = toJSONObject;
119             this.mFromJSONObject = fromJSONObject;
120         }
121 
122         /**
123          * @return {@link JSONObject} representation of the {@link Field}.
124          */
toJSONObject()125         JSONObject toJSONObject() throws JSONException {
126             JSONObject json = new JSONObject();
127             json.put(mName, mToJSONObject.apply(mValue));
128             return json;
129         }
130 
131         /**
132          * Populate the {@link Field#mName} and {@link Field#mValue} of the {@link Field} from its
133          * {@link JSONObject} representation.
134          */
fromJSONObject(JSONObject json, String key)135         void fromJSONObject(JSONObject json, String key) throws JSONException {
136             this.mName = key;
137             this.mValue = mFromJSONObject.apply(json, key);
138         }
139     }
140 
141     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
142     public static final String OWNER_KEY = "owner";
143     public static final String BUYER_KEY = "buyer";
144     public static final String AUCTION_SERVER_REQUEST_FLAGS_KEY = "auction_server_request_flags";
145     public static final String PRIORITY_KEY = "priority";
146     public static final String OMIT_ADS_VALUE = "omit_ads";
147     public static final String COMPONENT_ADS_KEY = "component_ads";
148     static final LinkedHashSet<String> mKeysSet =
149             new LinkedHashSet<>(
150                     Arrays.asList(
151                             OWNER_KEY,
152                             BUYER_KEY,
153                             NAME_KEY,
154                             ACTIVATION_TIME_KEY,
155                             EXPIRATION_TIME_KEY,
156                             DAILY_UPDATE_URI_KEY,
157                             BIDDING_LOGIC_URI_KEY,
158                             USER_BIDDING_SIGNALS_KEY,
159                             TRUSTED_BIDDING_DATA_KEY,
160                             ADS_KEY));
161     final LinkedHashMap<String, Field<?>> mFieldsMap = new LinkedHashMap<>();
162     private final ReadFiltersFromJsonStrategy mReadFiltersFromJsonStrategy;
163     private final ReadAdRenderIdFromJsonStrategy mReadAdRenderIdFromJsonStrategy;
164     private final boolean mAuctionServerRequestFlagsEnabled;
165     private final boolean mSellerConfigurationEnabled;
166     private final boolean mComponentAdsEnabled;
167     private final AdRenderIdValidator mComponentAdRenderIdValidator;
168     private final int mMaxNumComponentAds;
169 
CustomAudienceBlob( boolean frequencyCapFilteringEnabled, boolean appInstallFilteringEnabled, boolean adRenderIdEnabled, long adRenderIdMaxLength, boolean auctionServerRequestFlagsEnabled, boolean sellerConfigurationEnabled, boolean componentAdsEnabled, int componentAdRenderIdMaxLength, int maxNumComponentAds)170     public CustomAudienceBlob(
171             boolean frequencyCapFilteringEnabled,
172             boolean appInstallFilteringEnabled,
173             boolean adRenderIdEnabled,
174             long adRenderIdMaxLength,
175             boolean auctionServerRequestFlagsEnabled,
176             boolean sellerConfigurationEnabled,
177             boolean componentAdsEnabled,
178             int componentAdRenderIdMaxLength,
179             int maxNumComponentAds) {
180         mReadFiltersFromJsonStrategy =
181                 ReadFiltersFromJsonStrategyFactory.getStrategy(
182                         frequencyCapFilteringEnabled, appInstallFilteringEnabled);
183         mReadAdRenderIdFromJsonStrategy =
184                 ReadAdRenderIdFromJsonStrategyFactory.getStrategy(
185                         adRenderIdEnabled, adRenderIdMaxLength);
186         mAuctionServerRequestFlagsEnabled = auctionServerRequestFlagsEnabled;
187         mSellerConfigurationEnabled = sellerConfigurationEnabled;
188         mComponentAdsEnabled = componentAdsEnabled;
189         mComponentAdRenderIdValidator =
190                 AdRenderIdValidator.createEnabledInstance(componentAdRenderIdMaxLength);
191         mMaxNumComponentAds = maxNumComponentAds;
192     }
193 
194     @VisibleForTesting
CustomAudienceBlob()195     public CustomAudienceBlob() {
196         // TODO (b/356394210) Move this convenience method into a test fixture (or remove entirely)
197         // Filtering enabled by default.
198         this(
199                 true,
200                 true,
201                 true,
202                 FLEDGE_AUCTION_SERVER_AD_RENDER_ID_MAX_LENGTH,
203                 false,
204                 false,
205                 ENABLE_CUSTOM_AUDIENCE_COMPONENT_ADS,
206                 COMPONENT_AD_RENDER_ID_MAX_LENGTH_BYTES,
207                 MAX_COMPONENT_ADS_PER_CUSTOM_AUDIENCE);
208     }
209 
210     /** Update fields of the {@link CustomAudienceBlob} from a {@link JSONObject}. */
overrideFromJSONObject(JSONObject json)211     public void overrideFromJSONObject(JSONObject json) throws JSONException {
212         LinkedHashSet<String> jsonKeySet = new LinkedHashSet<>(Lists.newArrayList(json.keys()));
213         for (String key : mKeysSet) {
214             sLogger.v(key);
215             if (jsonKeySet.contains(key)) {
216                 sLogger.v("Adding %s", key);
217                 switch (key) {
218                     case OWNER_KEY:
219                         this.setOwner(this.getStringFromJSONObject(json, OWNER_KEY));
220                         break;
221                     case BUYER_KEY:
222                         this.setBuyer(
223                                 AdTechIdentifier.fromString(
224                                         this.getStringFromJSONObject(json, BUYER_KEY)));
225                         break;
226                     case NAME_KEY:
227                         this.setName(this.getStringFromJSONObject(json, NAME_KEY));
228                         break;
229                     case ACTIVATION_TIME_KEY:
230                         this.setActivationTime(
231                                 this.getInstantFromJSONObject(json, ACTIVATION_TIME_KEY));
232                         break;
233                     case EXPIRATION_TIME_KEY:
234                         this.setExpirationTime(
235                                 this.getInstantFromJSONObject(json, EXPIRATION_TIME_KEY));
236                         break;
237                     case DAILY_UPDATE_URI_KEY:
238                         this.setDailyUpdateUri(
239                                 Uri.parse(
240                                         this.getStringFromJSONObject(json, DAILY_UPDATE_URI_KEY)));
241                         break;
242                     case BIDDING_LOGIC_URI_KEY:
243                         this.setBiddingLogicUri(
244                                 Uri.parse(
245                                         this.getStringFromJSONObject(json, BIDDING_LOGIC_URI_KEY)));
246                         break;
247                     case USER_BIDDING_SIGNALS_KEY:
248                         this.setUserBiddingSignals(
249                                 AdSelectionSignals.fromString(
250                                         json.getJSONObject(USER_BIDDING_SIGNALS_KEY).toString()));
251                         break;
252                     case TRUSTED_BIDDING_DATA_KEY:
253                         this.setTrustedBiddingData(
254                                 this.getTrustedBiddingDataFromJSONObject(
255                                         json, TRUSTED_BIDDING_DATA_KEY));
256                         break;
257                     case ADS_KEY:
258                         this.setAds(this.getAdsFromJSONObject(json, ADS_KEY));
259                 }
260             }
261         }
262         // Set auction server flags if flag is enabled
263         if (mAuctionServerRequestFlagsEnabled
264                 && jsonKeySet.contains(AUCTION_SERVER_REQUEST_FLAGS_KEY)) {
265             this.setAuctionServerRequestFlags(
266                     this.getAuctionServerRequestFlagsFromJSONObject(
267                             json, AUCTION_SERVER_REQUEST_FLAGS_KEY));
268         }
269 
270         // Set priority if seller configuration flag is enabled
271         if (mSellerConfigurationEnabled && jsonKeySet.contains(PRIORITY_KEY)) {
272             this.setPriority(this.getDoubleFromJSONObject(json, PRIORITY_KEY));
273         }
274 
275         if (mComponentAdsEnabled && jsonKeySet.contains(COMPONENT_ADS_KEY)) {
276             this.setComponentAds(this.getComponentAdsFromJSONObject(json, COMPONENT_ADS_KEY));
277         }
278     }
279 
280     /**
281      * Update fields of the {@link CustomAudienceBlob} from a {@link
282      * FetchAndJoinCustomAudienceInput}.
283      */
overrideFromFetchAndJoinCustomAudienceInput(FetchAndJoinCustomAudienceInput input)284     public void overrideFromFetchAndJoinCustomAudienceInput(FetchAndJoinCustomAudienceInput input) {
285         this.setOwner(input.getCallerPackageName());
286         this.setBuyer(AdTechIdentifier.fromString(input.getFetchUri().getHost()));
287 
288         if (input.getName() != null) {
289             this.setName(input.getName());
290         }
291         if (input.getActivationTime() != null) {
292             this.setActivationTime(input.getActivationTime());
293         }
294         if (input.getExpirationTime() != null) {
295             this.setExpirationTime(input.getExpirationTime());
296         }
297         if (input.getUserBiddingSignals() != null) {
298             this.setUserBiddingSignals(input.getUserBiddingSignals());
299         }
300     }
301 
302     /**
303      * Utility methods to override a {@link CustomAudienceBlob} from a {@link PartialCustomAudience}
304      */
overrideFromPartialCustomAudience( String owner, AdTechIdentifier buyer, PartialCustomAudience partialCustomAudience)305     public void overrideFromPartialCustomAudience(
306             String owner, AdTechIdentifier buyer, PartialCustomAudience partialCustomAudience) {
307         this.setOwner(owner);
308         this.setBuyer(buyer);
309 
310         this.setName(partialCustomAudience.getName());
311 
312         if (partialCustomAudience.getActivationTime() != null) {
313             this.setActivationTime(partialCustomAudience.getActivationTime());
314         }
315 
316         if (partialCustomAudience.getExpirationTime() != null) {
317             this.setExpirationTime(partialCustomAudience.getExpirationTime());
318         }
319 
320         if (partialCustomAudience.getUserBiddingSignals() != null) {
321             this.setUserBiddingSignals(partialCustomAudience.getUserBiddingSignals());
322         }
323     }
324 
325     /**
326      * @return {@link JSONObject} representation of the {@link CustomAudienceBlob}.
327      */
asJSONObject()328     public JSONObject asJSONObject() {
329         JSONObject json = new JSONObject();
330         mFieldsMap.forEach(
331                 (name, field) -> {
332                     try {
333                         json.put(name, field.toJSONObject().get(name));
334                     } catch (JSONException e) {
335                         throw new RuntimeException(e);
336                     }
337                 });
338         return json;
339     }
340 
341     /**
342      * @return if the {@code owner} {@link Field} is set
343      */
hasOwner()344     public boolean hasOwner() {
345         return mFieldsMap.get(OWNER_KEY) != null;
346     }
347 
348     /**
349      * @return the {@code owner} {@link Field}
350      */
getOwner()351     public String getOwner() {
352         return (String) mFieldsMap.get(OWNER_KEY).mValue;
353     }
354 
355     /** set the {@code owner} {@link Field} */
setOwner(String value)356     public void setOwner(String value) {
357         if (mFieldsMap.containsKey(OWNER_KEY)) {
358             Field<String> field = (Field<String>) mFieldsMap.get(OWNER_KEY);
359             field.mValue = value;
360         } else {
361             Field<String> field = new Field<>((str) -> str, this::getStringFromJSONObject);
362 
363             field.mName = OWNER_KEY;
364             field.mValue = value;
365 
366             mFieldsMap.put(OWNER_KEY, field);
367         }
368     }
369 
370     /**
371      * @return if the {@code buyer} {@link Field} is set
372      */
hasBuyer()373     public boolean hasBuyer() {
374         return mFieldsMap.get(BUYER_KEY) != null;
375     }
376 
377     /**
378      * @return the {@code buyer} {@link Field}
379      */
getBuyer()380     public AdTechIdentifier getBuyer() {
381         return (AdTechIdentifier) mFieldsMap.get(BUYER_KEY).mValue;
382     }
383 
384     /** set the {@code buyer} {@link Field} */
setBuyer(AdTechIdentifier value)385     public void setBuyer(AdTechIdentifier value) {
386         if (mFieldsMap.containsKey(BUYER_KEY)) {
387             Field<AdTechIdentifier> field = (Field<AdTechIdentifier>) mFieldsMap.get(BUYER_KEY);
388             field.mValue = value;
389         } else {
390             Field<AdTechIdentifier> field =
391                     new Field<>(
392                             Object::toString,
393                             (json, key) ->
394                                     AdTechIdentifier.fromString(
395                                             this.getStringFromJSONObject(json, key)));
396 
397             field.mName = BUYER_KEY;
398             field.mValue = value;
399 
400             mFieldsMap.put(BUYER_KEY, field);
401         }
402     }
403 
404     /** sets the {@code auctionServerRequestFlags} {@link Field} */
setAuctionServerRequestFlags(List<String> auctionServerRequestFlags)405     public void setAuctionServerRequestFlags(List<String> auctionServerRequestFlags) {
406         if (mFieldsMap.containsKey(AUCTION_SERVER_REQUEST_FLAGS_KEY)) {
407             Field<List<String>> field =
408                     (Field<List<String>>) mFieldsMap.get(AUCTION_SERVER_REQUEST_FLAGS_KEY);
409             field.mValue = auctionServerRequestFlags;
410         } else {
411             Field<List<String>> field =
412                     new Field<>(
413                             this::getAuctionServerRequestFlagsAsJSONArray,
414                             this::getAuctionServerRequestFlagsFromJSONObject);
415 
416             field.mName = AUCTION_SERVER_REQUEST_FLAGS_KEY;
417             field.mValue = auctionServerRequestFlags;
418 
419             mFieldsMap.put(AUCTION_SERVER_REQUEST_FLAGS_KEY, field);
420         }
421     }
422 
423     /** Returns the server auction request bitfield extracted from the response, if found. */
424     @CustomAudience.AuctionServerRequestFlag
getAuctionServerRequestFlags()425     public int getAuctionServerRequestFlags() {
426         @CustomAudience.AuctionServerRequestFlag int result = 0;
427         if (mFieldsMap.containsKey(AUCTION_SERVER_REQUEST_FLAGS_KEY)) {
428             sLogger.d("Fields map contains auction server key");
429             List<String> list =
430                     (List<String>) mFieldsMap.get(AUCTION_SERVER_REQUEST_FLAGS_KEY).mValue;
431             for (String s : list) {
432                 if (OMIT_ADS_VALUE.equals(s)) {
433                     if ((result & FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS) == 0) {
434                         // Only set the flag once
435                         result = result | FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS;
436                     }
437                 }
438             }
439         }
440         return result;
441     }
442 
443     /**
444      * @return if the {@code name} {@link Field} is set
445      */
hasName()446     public boolean hasName() {
447         return mFieldsMap.get(NAME_KEY) != null;
448     }
449 
450     /**
451      * @return the {@code name} {@link Field}
452      */
getName()453     public String getName() {
454         return (String) mFieldsMap.get(NAME_KEY).mValue;
455     }
456 
457     /** set the {@code name} {@link Field} */
setName(String value)458     public void setName(String value) {
459         if (mFieldsMap.containsKey(NAME_KEY)) {
460             Field<String> field = (Field<String>) mFieldsMap.get(NAME_KEY);
461             field.mValue = value;
462         } else {
463             Field<String> field = new Field<>((str) -> str, this::getStringFromJSONObject);
464 
465             field.mName = NAME_KEY;
466             field.mValue = value;
467 
468             mFieldsMap.put(NAME_KEY, field);
469         }
470     }
471 
472     /**
473      * @return if the {@code activationTime} {@link Field} is set
474      */
hasActivationTime()475     public boolean hasActivationTime() {
476         return mFieldsMap.get(ACTIVATION_TIME_KEY) != null;
477     }
478 
479     /**
480      * @return the {@code activationTime} {@link Field}
481      */
getActivationTime()482     public Instant getActivationTime() {
483         return (Instant) mFieldsMap.get(ACTIVATION_TIME_KEY).mValue;
484     }
485 
486     /** set the {@code activationTime} {@link Field} */
setActivationTime(Instant value)487     public void setActivationTime(Instant value) {
488         if (mFieldsMap.containsKey(ACTIVATION_TIME_KEY)) {
489             Field<Instant> field = (Field<Instant>) mFieldsMap.get(ACTIVATION_TIME_KEY);
490             field.mValue = value;
491         } else {
492             Field<Instant> field =
493                     new Field<>(Instant::toEpochMilli, this::getInstantFromJSONObject);
494 
495             field.mName = ACTIVATION_TIME_KEY;
496             field.mValue = value;
497 
498             mFieldsMap.put(ACTIVATION_TIME_KEY, field);
499         }
500     }
501 
502     /**
503      * @return if the {@code expirationTime} {@link Field} is set
504      */
hasExpirationTime()505     public boolean hasExpirationTime() {
506         return mFieldsMap.get(EXPIRATION_TIME_KEY) != null;
507     }
508 
509     /**
510      * @return the {@code expirationTime} {@link Field}
511      */
getExpirationTime()512     public Instant getExpirationTime() {
513         return (Instant) mFieldsMap.get(EXPIRATION_TIME_KEY).mValue;
514     }
515 
516     /** set the {@code expirationTime} {@link Field} */
setExpirationTime(Instant value)517     public void setExpirationTime(Instant value) {
518         if (mFieldsMap.containsKey(EXPIRATION_TIME_KEY)) {
519             Field<Instant> field = (Field<Instant>) mFieldsMap.get(EXPIRATION_TIME_KEY);
520             field.mValue = value;
521         } else {
522             Field<Instant> field =
523                     new Field<>(Instant::toEpochMilli, this::getInstantFromJSONObject);
524 
525             field.mName = EXPIRATION_TIME_KEY;
526             field.mValue = value;
527 
528             mFieldsMap.put(EXPIRATION_TIME_KEY, field);
529         }
530     }
531 
532     /**
533      * @return if the {@code dailyUpdateUri} {@link Field} is set
534      */
hasDailyUpdateUri()535     public boolean hasDailyUpdateUri() {
536         return mFieldsMap.get(DAILY_UPDATE_URI_KEY) != null;
537     }
538 
539     /**
540      * @return the {@code dailyUpdateUri} {@link Field}
541      */
getDailyUpdateUri()542     public Uri getDailyUpdateUri() {
543         return (Uri) mFieldsMap.get(DAILY_UPDATE_URI_KEY).mValue;
544     }
545 
546     /** set the {@code dailyUpdateUri} {@link Field} */
setDailyUpdateUri(Uri value)547     public void setDailyUpdateUri(Uri value) {
548         if (mFieldsMap.containsKey(DAILY_UPDATE_URI_KEY)) {
549             Field<Uri> field = (Field<Uri>) mFieldsMap.get(DAILY_UPDATE_URI_KEY);
550             field.mValue = value;
551         } else {
552             Field<Uri> field =
553                     new Field<>(
554                             Uri::toString,
555                             (json, key) -> Uri.parse(this.getStringFromJSONObject(json, key)));
556 
557             field.mName = DAILY_UPDATE_URI_KEY;
558             field.mValue = value;
559 
560             mFieldsMap.put(DAILY_UPDATE_URI_KEY, field);
561         }
562     }
563 
564     /**
565      * @return if the {@code biddingLogicUri} {@link Field} is set
566      */
hasBiddingLogicUri()567     public boolean hasBiddingLogicUri() {
568         return mFieldsMap.get(BIDDING_LOGIC_URI_KEY) != null;
569     }
570 
571     /**
572      * @return the {@code biddingLogicUri} {@link Field}
573      */
getBiddingLogicUri()574     public Uri getBiddingLogicUri() {
575         return (Uri) mFieldsMap.get(BIDDING_LOGIC_URI_KEY).mValue;
576     }
577 
578     /** set the {@code biddingLogicUri} {@link Field} */
setBiddingLogicUri(Uri value)579     public void setBiddingLogicUri(Uri value) {
580         if (mFieldsMap.containsKey(BIDDING_LOGIC_URI_KEY)) {
581             Field<Uri> field = (Field<Uri>) mFieldsMap.get(BIDDING_LOGIC_URI_KEY);
582             field.mValue = value;
583         } else {
584             Field<Uri> field =
585                     new Field<>(
586                             Uri::toString,
587                             (json, key) -> Uri.parse(this.getStringFromJSONObject(json, key)));
588 
589             field.mName = BIDDING_LOGIC_URI_KEY;
590             field.mValue = value;
591 
592             mFieldsMap.put(BIDDING_LOGIC_URI_KEY, field);
593         }
594     }
595 
596     /**
597      * @return if the {@code userBiddingSignals} {@link Field} is set
598      */
hasUserBiddingSignals()599     public boolean hasUserBiddingSignals() {
600         return mFieldsMap.get(USER_BIDDING_SIGNALS_KEY) != null;
601     }
602 
603     /**
604      * @return the {@code userBiddingSignals} {@link Field}
605      */
getUserBiddingSignals()606     public AdSelectionSignals getUserBiddingSignals() {
607         return (AdSelectionSignals) mFieldsMap.get(USER_BIDDING_SIGNALS_KEY).mValue;
608     }
609 
610     /** set the {@code userBiddingSignals} {@link Field} */
setUserBiddingSignals(AdSelectionSignals value)611     public void setUserBiddingSignals(AdSelectionSignals value) {
612         if (mFieldsMap.containsKey(USER_BIDDING_SIGNALS_KEY)) {
613             Field<AdSelectionSignals> field =
614                     (Field<AdSelectionSignals>) mFieldsMap.get(USER_BIDDING_SIGNALS_KEY);
615             field.mValue = value;
616         } else {
617             Field<AdSelectionSignals> field =
618                     new Field<>(
619                             (adSelectionSignals) -> {
620                                 try {
621                                     return new JSONObject(adSelectionSignals.toString());
622                                 } catch (JSONException e) {
623                                     throw new RuntimeException(e);
624                                 }
625                             },
626                             (json, key) -> {
627                                 try {
628                                     return AdSelectionSignals.fromString(
629                                             json.getJSONObject(key).toString());
630                                 } catch (JSONException e) {
631                                     throw new RuntimeException(e);
632                                 }
633                             });
634 
635             field.mName = USER_BIDDING_SIGNALS_KEY;
636             field.mValue = value;
637 
638             mFieldsMap.put(USER_BIDDING_SIGNALS_KEY, field);
639         }
640     }
641 
642     /**
643      * @return if the {@code trustedBiddingData} {@link Field} is set
644      */
hasTrustedBiddingData()645     public boolean hasTrustedBiddingData() {
646         return mFieldsMap.get(TRUSTED_BIDDING_DATA_KEY) != null;
647     }
648 
649     /**
650      * @return the {@code trustedBiddingData} {@link Field}
651      */
getTrustedBiddingData()652     public TrustedBiddingData getTrustedBiddingData() {
653         return (TrustedBiddingData) mFieldsMap.get(TRUSTED_BIDDING_DATA_KEY).mValue;
654     }
655 
656     /** set the {@code trustedBiddingData} {@link Field} */
setTrustedBiddingData(TrustedBiddingData value)657     public void setTrustedBiddingData(TrustedBiddingData value) {
658         if (mFieldsMap.containsKey(TRUSTED_BIDDING_DATA_KEY)) {
659             Field<TrustedBiddingData> field =
660                     (Field<TrustedBiddingData>) mFieldsMap.get(TRUSTED_BIDDING_DATA_KEY);
661             field.mValue = value;
662         } else {
663             Field<TrustedBiddingData> field =
664                     new Field<>(
665                             this::getTrustedBiddingDataAsJSONObject,
666                             this::getTrustedBiddingDataFromJSONObject);
667 
668             field.mName = TRUSTED_BIDDING_DATA_KEY;
669             field.mValue = value;
670 
671             mFieldsMap.put(TRUSTED_BIDDING_DATA_KEY, field);
672         }
673     }
674 
getTrustedBiddingDataAsJSONObject(TrustedBiddingData value)675     private JSONObject getTrustedBiddingDataAsJSONObject(TrustedBiddingData value) {
676         try {
677             JSONObject json = new JSONObject();
678             json.put(TRUSTED_BIDDING_URI_KEY, value.getTrustedBiddingUri().toString());
679             json.put(TRUSTED_BIDDING_KEYS_KEY, new JSONArray(value.getTrustedBiddingKeys()));
680             return json;
681         } catch (JSONException e) {
682             throw new RuntimeException(e);
683         }
684     }
685 
getTrustedBiddingDataFromJSONObject(JSONObject json, String key)686     private TrustedBiddingData getTrustedBiddingDataFromJSONObject(JSONObject json, String key) {
687         return getValueFromJSONObject(
688                 json,
689                 key,
690                 (jsonObject, jsonKey) -> {
691                     try {
692                         JSONObject dataJsonObj = jsonObject.getJSONObject(jsonKey);
693 
694                         String uri =
695                                 JsonUtils.getStringFromJson(
696                                         dataJsonObj,
697                                         TRUSTED_BIDDING_URI_KEY,
698                                         String.format(
699                                                 STRING_ERROR_FORMAT, TRUSTED_BIDDING_URI_KEY, key));
700                         Uri parsedUri = Uri.parse(uri);
701 
702                         JSONArray keysJsonArray =
703                                 dataJsonObj.getJSONArray(TRUSTED_BIDDING_KEYS_KEY);
704                         int keysListLength = keysJsonArray.length();
705                         List<String> keysList = new ArrayList<>(keysListLength);
706                         for (int i = 0; i < keysListLength; i++) {
707                             try {
708                                 keysList.add(
709                                         JsonUtils.getStringFromJsonArrayAtIndex(
710                                                 keysJsonArray,
711                                                 i,
712                                                 String.format(
713                                                         STRING_ERROR_FORMAT,
714                                                         TRUSTED_BIDDING_KEYS_KEY,
715                                                         key)));
716                             } catch (JSONException | NullPointerException exception) {
717                                 // Skip any keys that are malformed and continue to the next in the
718                                 // list; note that if the entire given list of keys is junk, then
719                                 // any existing trusted bidding keys are cleared from the custom
720                                 // audience
721                                 sLogger.v(
722                                         SKIP_INVALID_JSON_TYPE_LOG_FORMAT,
723                                         json.hashCode(),
724                                         TRUSTED_BIDDING_KEYS_KEY,
725                                         Optional.ofNullable(exception.getMessage())
726                                                 .orElse("<null>"));
727                             }
728                         }
729 
730                         TrustedBiddingData trustedBiddingData =
731                                 new TrustedBiddingData.Builder()
732                                         .setTrustedBiddingUri(parsedUri)
733                                         .setTrustedBiddingKeys(keysList)
734                                         .build();
735 
736                         return trustedBiddingData;
737                     } catch (JSONException e) {
738                         throw new RuntimeException(e);
739                     }
740                 });
741     }
742 
743     /**
744      * @return if the {@code ads} {@link Field} is set
745      */
746     public boolean hasAds() {
747         return mFieldsMap.get(ADS_KEY) != null;
748     }
749 
750     /**
751      * @return the {@code ads} {@link Field}
752      */
753     public List<AdData> getAds() {
754         return (List<AdData>) mFieldsMap.get(ADS_KEY).mValue;
755     }
756 
757     /** set the {@code ads} {@link Field} */
758     public void setAds(List<AdData> value) {
759         if (mFieldsMap.containsKey(ADS_KEY)) {
760             Field<List<AdData>> field = (Field<List<AdData>>) mFieldsMap.get(ADS_KEY);
761             field.mValue = value;
762         } else {
763             Field<List<AdData>> field =
764                     new Field<>(this::getAdsAsJSONObject, this::getAdsFromJSONObject);
765 
766             field.mName = ADS_KEY;
767             field.mValue = value;
768 
769             mFieldsMap.put(ADS_KEY, field);
770         }
771     }
772 
773     /**
774      * @return the {@code priority} {@link Field}
775      */
776     public double getPriority() {
777         double result = PRIORITY_DEFAULT;
778         if (mFieldsMap.containsKey(PRIORITY_KEY)) {
779             result = (double) mFieldsMap.get(PRIORITY_KEY).mValue;
780         }
781         return result;
782     }
783 
784     /** set the {@code priority} {@link Field} */
785     public void setPriority(double value) {
786         if (mFieldsMap.containsKey(PRIORITY_KEY)) {
787             Field<Double> field = (Field<Double>) mFieldsMap.get(PRIORITY_KEY);
788             field.mValue = value;
789         } else {
790             Field<Double> field = new Field<>((dbl) -> dbl, this::getDoubleFromJSONObject);
791 
792             field.mName = PRIORITY_KEY;
793             field.mValue = value;
794 
795             mFieldsMap.put(PRIORITY_KEY, field);
796         }
797     }
798 
799     /** set the {@code componentAds} {@link Field} */
800     public void setComponentAds(List<ComponentAdData> value) {
801         if (mFieldsMap.containsKey(COMPONENT_ADS_KEY)) {
802             Field<List<ComponentAdData>> field =
803                     (Field<List<ComponentAdData>>) mFieldsMap.get(COMPONENT_ADS_KEY);
804             field.mValue = value;
805         } else {
806             Field<List<ComponentAdData>> field =
807                     new Field<>(
808                             this::getComponentAdsAdsAsJSONObject,
809                             this::getComponentAdsFromJSONObject);
810 
811             field.mName = COMPONENT_ADS_KEY;
812             field.mValue = value;
813 
814             mFieldsMap.put(COMPONENT_ADS_KEY, field);
815         }
816     }
817 
818     /**
819      * @return the {@code componentAds} {@link Field}
820      */
821     public List<ComponentAdData> getComponentAds() {
822         if (mFieldsMap.containsKey(COMPONENT_ADS_KEY)) {
823             return (List<ComponentAdData>) mFieldsMap.get(COMPONENT_ADS_KEY).mValue;
824         }
825         return List.of();
826     }
827 
828     private JSONArray getAdsAsJSONObject(List<AdData> value) {
829         try {
830             JSONArray adsJson = new JSONArray();
831             for (AdData ad : value) {
832                 JSONObject adJson = new JSONObject();
833 
834                 adJson.put(RENDER_URI_KEY, ad.getRenderUri().toString());
835                 try {
836                     adJson.put(METADATA_KEY, new JSONObject(ad.getMetadata()));
837                 } catch (JSONException exception) {
838                     sLogger.v(
839                             "Trying to add invalid JSON to test object (%s); inserting as String"
840                                     + " instead",
841                             exception.getMessage());
842                     adJson.put(METADATA_KEY, ad.getMetadata());
843                 }
844                 if (!ad.getAdCounterKeys().isEmpty()) {
845                     adJson.put(AD_COUNTERS_KEY, new JSONArray(ad.getAdCounterKeys()));
846                 }
847                 if (ad.getAdFilters() != null) {
848                     adJson.put(AD_FILTERS_KEY, ad.getAdFilters().toJson());
849                 }
850                 if (ad.getAdRenderId() != null) {
851                     adJson.put(AD_RENDER_ID_KEY, ad.getAdRenderId());
852                 }
853                 adsJson.put(adJson);
854             }
855             return adsJson;
856         } catch (JSONException e) {
857             throw new RuntimeException(e);
858         }
859     }
860 
861     private JSONArray getComponentAdsAdsAsJSONObject(List<ComponentAdData> componentAds) {
862         try {
863             JSONArray componentAdsJson = new JSONArray();
864             for (ComponentAdData componentAd : componentAds) {
865                 JSONObject componentAdJson = new JSONObject();
866 
867                 componentAdJson.put(RENDER_URI_KEY, componentAd.getRenderUri().toString());
868                 componentAdJson.put(AD_RENDER_ID_KEY, componentAd.getAdRenderId());
869 
870                 componentAdsJson.put(componentAdJson);
871             }
872             return componentAdsJson;
873         } catch (JSONException e) {
874             // Don't want to throw an exception since this is an optional field
875             return new JSONArray();
876         }
877     }
878 
879     private JSONArray getAuctionServerRequestFlagsAsJSONArray(List<String> value) {
880         return new JSONArray(value);
881     }
882 
883     @SuppressWarnings("FormatStringAnnotation")
884     private List<String> getAuctionServerRequestFlagsFromJSONObject(JSONObject json, String key) {
885         return getValueFromJSONObject(
886                 json,
887                 key,
888                 (jsonObject, jsonKey) -> {
889                     List<String> resultList = new ArrayList<>();
890                     try {
891                         JSONArray flagsJsonArray = jsonObject.getJSONArray(key);
892                         for (int i = 0; i < flagsJsonArray.length(); i++) {
893                             try {
894                                 resultList.add(
895                                         JsonUtils.getStringFromJsonArrayAtIndex(
896                                                 flagsJsonArray,
897                                                 i,
898                                                 SKIP_INVALID_JSON_TYPE_LOG_FORMAT));
899                             } catch (JSONException e) {
900                                 sLogger.v(
901                                         SKIP_INVALID_JSON_TYPE_LOG_FORMAT,
902                                         jsonObject.hashCode(),
903                                         key,
904                                         Optional.ofNullable(e.getMessage()).orElse("<null>"));
905                             }
906                         }
907                     } catch (JSONException e) {
908                         // Ignore since we don't want to fail if there is an issue with this
909                         // optional field
910                         sLogger.v(
911                                 FIELD_NOT_FOUND_LOG_FORMAT,
912                                 jsonObject.hashCode(),
913                                 key,
914                                 Optional.ofNullable(e.getMessage()).orElse("<null>"));
915                     }
916                     return resultList;
917                 });
918     }
919 
920     private List<AdData> getAdsFromJSONObject(JSONObject json, String key) {
921         return getValueFromJSONObject(
922                 json,
923                 key,
924                 (jsonObject, jsonKey) -> {
925                     try {
926                         JSONArray adsJsonArray = jsonObject.getJSONArray(key);
927                         int adsListLength = adsJsonArray.length();
928                         List<AdData> adsList = new ArrayList<>();
929                         for (int i = 0; i < adsListLength; i++) {
930                             JSONObject adDataJsonObj = adsJsonArray.getJSONObject(i);
931 
932                             // Note: getString() coerces values to be strings; use get() instead
933                             Object uri = adDataJsonObj.get(RENDER_URI_KEY);
934                             if (!(uri instanceof String)) {
935                                 throw new JSONException(
936                                         "Unexpected format parsing "
937                                                 + RENDER_URI_KEY
938                                                 + " in "
939                                                 + key);
940                             }
941                             Uri parsedUri = Uri.parse(Objects.requireNonNull((String) uri));
942 
943                             String metadata =
944                                     Objects.requireNonNull(
945                                                     adDataJsonObj.getJSONObject(METADATA_KEY))
946                                             .toString();
947 
948                             DBAdData.Builder adDataBuilder =
949                                     new DBAdData.Builder()
950                                             .setRenderUri(parsedUri)
951                                             .setMetadata(metadata);
952 
953                             mReadFiltersFromJsonStrategy.readFilters(adDataBuilder, adDataJsonObj);
954                             mReadAdRenderIdFromJsonStrategy.readId(adDataBuilder, adDataJsonObj);
955 
956                             DBAdData dbAdData = adDataBuilder.build();
957                             AdData adData =
958                                     new AdData.Builder()
959                                             .setMetadata(dbAdData.getMetadata())
960                                             .setRenderUri(dbAdData.getRenderUri())
961                                             .setAdCounterKeys(dbAdData.getAdCounterKeys())
962                                             .setAdFilters(dbAdData.getAdFilters())
963                                             .setAdRenderId(dbAdData.getAdRenderId())
964                                             .build();
965 
966                             adsList.add(adData);
967                         }
968                         return adsList;
969                     } catch (JSONException e) {
970                         throw new RuntimeException(e);
971                     }
972                 });
973     }
974 
975     private List<ComponentAdData> getComponentAdsFromJSONObject(JSONObject json, String key) {
976         return getValueFromJSONObject(
977                 json,
978                 key,
979                 (jsonObject, jsonKey) -> {
980                     try {
981                         AdTechIdentifier buyer =
982                                 AdTechIdentifier.fromString(
983                                         this.getStringFromJSONObject(json, BUYER_KEY));
984 
985                         JSONArray componentAdsJsonArray = jsonObject.getJSONArray(key);
986 
987                         int componentAdsListLength = componentAdsJsonArray.length();
988                         // TODO(b/381392728):investigate whether we need an overall size on list of
989                         // component
990                         if (componentAdsListLength > mMaxNumComponentAds) {
991                             sLogger.v(COMPONENT_ADS_SIZE_EXCEEDS_MAX);
992                             throw new IllegalArgumentException(COMPONENT_ADS_SIZE_EXCEEDS_MAX);
993                         }
994                         List<ComponentAdData> componentAdsList = new ArrayList<>();
995                         for (int i = 0; i < componentAdsListLength; i++) {
996                             try {
997                                 JSONObject componentAdDataJsonObj =
998                                         componentAdsJsonArray.getJSONObject(i);
999 
1000                                 // Note: getString() coerces values to be strings; use get() instead
1001                                 Object uri = componentAdDataJsonObj.get(RENDER_URI_KEY);
1002                                 if (!(uri instanceof String)) {
1003                                     throw new JSONException(
1004                                             "Unexpected format parsing "
1005                                                     + RENDER_URI_KEY
1006                                                     + " in "
1007                                                     + COMPONENT_ADS_KEY);
1008                                 }
1009                                 Uri parsedUri = Uri.parse(Objects.requireNonNull((String) uri));
1010 
1011                                 AdTechUriValidator uriValidator =
1012                                         new AdTechUriValidator(
1013                                                 ValidatorUtil.AD_TECH_ROLE_BUYER,
1014                                                 buyer.toString(),
1015                                                 this.getClass().getSimpleName(),
1016                                                 RENDER_URI_KEY);
1017                                 uriValidator.validate(parsedUri);
1018 
1019                                 Object adRenderId = componentAdDataJsonObj.get(AD_RENDER_ID_KEY);
1020                                 if (!(adRenderId instanceof String adRenderIdString)) {
1021                                     throw new JSONException(
1022                                             "Unexpected format parsing "
1023                                                     + AD_RENDER_ID_KEY
1024                                                     + " in "
1025                                                     + COMPONENT_ADS_KEY);
1026                                 }
1027                                 mComponentAdRenderIdValidator.validate(adRenderIdString);
1028 
1029                                 ComponentAdData componentAdData =
1030                                         new ComponentAdData(parsedUri, adRenderIdString);
1031                                 componentAdsList.add(componentAdData);
1032                             } catch (JSONException
1033                                     | NullPointerException
1034                                     | IllegalArgumentException exception) {
1035                                 // We don't want to fail since this is an optional field,
1036                                 return new ArrayList<>();
1037                             }
1038                         }
1039                         return componentAdsList;
1040                     } catch (JSONException | IllegalArgumentException e) {
1041                         // Ignore since we don't want to fail if there is an issue with this
1042                         // optional field
1043                         return new ArrayList<>();
1044                     }
1045                 });
1046     }
1047 
1048     private String getStringFromJSONObject(JSONObject json, String key) {
1049         return getValueFromJSONObject(
1050                 json,
1051                 key,
1052                 (jsonObject, jsonKey) -> {
1053                     try {
1054                         return JsonUtils.getStringFromJson(
1055                                 json,
1056                                 key,
1057                                 String.format(STRING_ERROR_FORMAT, key, json.hashCode()));
1058                     } catch (JSONException e) {
1059                         throw new RuntimeException(e);
1060                     }
1061                 });
1062     }
1063 
1064     private Double getDoubleFromJSONObject(JSONObject json, String key) {
1065         return getValueFromJSONObject(
1066                 json,
1067                 key,
1068                 (jsonObject, jsonKey) -> {
1069                     double priority = PRIORITY_DEFAULT;
1070                     try {
1071                         priority = jsonObject.getDouble(jsonKey);
1072                     } catch (JSONException e) {
1073                         // Ignore since we don't want to fail if there is an issue with this
1074                         // optional field
1075                         sLogger.d(String.valueOf(e));
1076                     }
1077                     return priority;
1078                 });
1079     }
1080 
1081     private Instant getInstantFromJSONObject(JSONObject json, String key) {
1082         return getValueFromJSONObject(
1083                 json,
1084                 key,
1085                 (jsonObject, jsonKey) -> {
1086                     try {
1087                         return Instant.ofEpochMilli(jsonObject.getLong(jsonKey));
1088                     } catch (Exception e) {
1089                         throw new RuntimeException(e);
1090                     }
1091                 });
1092     }
1093 
1094     private <T> T getValueFromJSONObject(
1095             JSONObject json, String key, BiFunction<JSONObject, String, T> fromJsonObject) {
1096         if (json.has(key)) {
1097             sLogger.v(FIELD_FOUND_LOG_FORMAT, json.hashCode(), key);
1098             return fromJsonObject.apply(json, key);
1099         } else {
1100             sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, json.hashCode(), ACTIVATION_TIME_KEY);
1101             return null;
1102         }
1103     }
1104 }
1105