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