1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.adservices.service.customaudience;
18 
19 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.FIELD_FOUND_LOG_FORMAT;
20 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.FIELD_NOT_FOUND_LOG_FORMAT;
21 import static com.android.adservices.service.customaudience.CustomAudienceUpdatableDataReader.STRING_ERROR_FORMAT;
22 
23 import android.adservices.common.AdSelectionSignals;
24 import android.adservices.common.AdTechIdentifier;
25 import android.net.Uri;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import com.android.adservices.LoggerFactory;
31 import com.android.adservices.data.common.DBAdData;
32 import com.android.adservices.data.customaudience.DBTrustedBiddingData;
33 import com.android.adservices.service.common.JsonUtils;
34 
35 import org.json.JSONException;
36 import org.json.JSONObject;
37 
38 import java.time.Instant;
39 import java.util.List;
40 import java.util.Objects;
41 
42 // TODO(b/283857101): Delete and use CustomAudienceBlob instead.
43 /** A parser and validator for a JSON representation of a Custom Audience. */
44 public class FetchCustomAudienceReader {
45     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
46     public static final String NAME_KEY = "name";
47     public static final String ACTIVATION_TIME_KEY = "activation_time";
48     public static final String EXPIRATION_TIME_KEY = "expiration_time";
49     public static final String DAILY_UPDATE_URI_KEY = "daily_update_uri";
50     public static final String BIDDING_LOGIC_URI_KEY = "bidding_logic_uri";
51     private final JSONObject mResponseObject;
52     private final String mResponseHash;
53     private final AdTechIdentifier mBuyer;
54     private final CustomAudienceUpdatableDataReader mCustomAudienceUpdatableDataReader;
55 
56     /**
57      * Creates a {@link FetchCustomAudienceReader} that will read updatable data from a given {@link
58      * JSONObject} and log with the given identifying {@code responseHash}.
59      *
60      * @param responseObject a {@link JSONObject} that may contain user bidding signals, trusted
61      *     bidding data, and/or a list of ads
62      * @param responseHash a String that uniquely identifies the response which is used in logging
63      * @param buyer the buyer ad tech's eTLD+1
64      * @param maxUserBiddingSignalsSizeB the configured maximum size in bytes allocated for user
65      *     bidding signals
66      * @param maxTrustedBiddingDataSizeB the configured maximum size in bytes allocated for trusted
67      *     bidding data
68      * @param maxAdsSizeB the configured maximum size in bytes allocated for ads
69      * @param maxNumAds the configured maximum number of ads allowed per update
70      * @param frequencyCapFilteringEnabled whether or not frequency cap filtering fields should be
71      *     read
72      * @param appInstallFilteringEnabled whether or not app install filtering fields should be read
73      */
FetchCustomAudienceReader( @onNull JSONObject responseObject, @NonNull String responseHash, @NonNull AdTechIdentifier buyer, int maxUserBiddingSignalsSizeB, int maxTrustedBiddingDataSizeB, int maxAdsSizeB, int maxNumAds, boolean frequencyCapFilteringEnabled, boolean appInstallFilteringEnabled, boolean adRenderIdEnabled, long adRenderIdMaxLength)74     protected FetchCustomAudienceReader(
75             @NonNull JSONObject responseObject,
76             @NonNull String responseHash,
77             @NonNull AdTechIdentifier buyer,
78             int maxUserBiddingSignalsSizeB,
79             int maxTrustedBiddingDataSizeB,
80             int maxAdsSizeB,
81             int maxNumAds,
82             boolean frequencyCapFilteringEnabled,
83             boolean appInstallFilteringEnabled,
84             boolean adRenderIdEnabled,
85             long adRenderIdMaxLength) {
86         Objects.requireNonNull(responseObject);
87         Objects.requireNonNull(responseHash);
88         Objects.requireNonNull(buyer);
89 
90         mResponseObject = responseObject;
91         mResponseHash = responseHash;
92         mBuyer = buyer;
93         mCustomAudienceUpdatableDataReader =
94                 new CustomAudienceUpdatableDataReader(
95                         responseObject,
96                         responseHash,
97                         buyer,
98                         maxUserBiddingSignalsSizeB,
99                         maxTrustedBiddingDataSizeB,
100                         maxAdsSizeB,
101                         maxNumAds,
102                         frequencyCapFilteringEnabled,
103                         appInstallFilteringEnabled,
104                         adRenderIdEnabled,
105                         adRenderIdMaxLength);
106     }
107 
108     /**
109      * Returns the user bidding signals extracted from the input object, if found.
110      *
111      * @throws JSONException if the key is found but the schema is incorrect
112      * @throws NullPointerException if the key found by the field is null
113      * @throws IllegalArgumentException if the extracted signals fail data validation
114      */
115     @Nullable
getNameFromJsonObject()116     public String getNameFromJsonObject()
117             throws JSONException, NullPointerException, IllegalArgumentException {
118         if (mResponseObject.has(NAME_KEY)) {
119             sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, NAME_KEY);
120 
121             String name =
122                     JsonUtils.getStringFromJson(
123                             mResponseObject,
124                             NAME_KEY,
125                             String.format(STRING_ERROR_FORMAT, NAME_KEY, mResponseHash));
126 
127             // TODO(b/282018172): Validate name field
128             // sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, NAME_KEY);
129             return name;
130         } else {
131             sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, NAME_KEY);
132             return null;
133         }
134     }
135 
136     /**
137      * Returns the user bidding signals extracted from the input object, if found.
138      *
139      * @throws JSONException if the key is found but the schema is incorrect
140      * @throws NullPointerException if the key found by the field is null
141      * @throws IllegalArgumentException if the extracted signals fail data validation
142      */
143     @Nullable
getActivationTimeFromJsonObject()144     public Instant getActivationTimeFromJsonObject()
145             throws JSONException, NullPointerException, IllegalArgumentException {
146         if (mResponseObject.has(ACTIVATION_TIME_KEY)) {
147             sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, ACTIVATION_TIME_KEY);
148 
149             Instant activationTime =
150                     Instant.ofEpochMilli(mResponseObject.getLong(ACTIVATION_TIME_KEY));
151 
152             // TODO(b/282018172): Validate activation_time field
153             // sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, ACTIVATION_TIME_KEY);
154             return activationTime;
155         } else {
156             sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, ACTIVATION_TIME_KEY);
157             return null;
158         }
159     }
160 
161     /**
162      * Returns the user bidding signals extracted from the input object, if found.
163      *
164      * @throws JSONException if the key is found but the schema is incorrect
165      * @throws NullPointerException if the key found by the field is null
166      * @throws IllegalArgumentException if the extracted signals fail data validation
167      */
168     @Nullable
getExpirationTimeFromJsonObject()169     public Instant getExpirationTimeFromJsonObject()
170             throws JSONException, NullPointerException, IllegalArgumentException {
171         if (mResponseObject.has(EXPIRATION_TIME_KEY)) {
172             sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, EXPIRATION_TIME_KEY);
173             Instant expirationTime =
174                     Instant.ofEpochMilli(mResponseObject.getLong(EXPIRATION_TIME_KEY));
175 
176             // TODO(b/282018172): Validate expiration_time field
177             // sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, EXPIRATION_TIME_KEY);
178             return expirationTime;
179         } else {
180             sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, EXPIRATION_TIME_KEY);
181             return null;
182         }
183     }
184 
185     /**
186      * Returns the user bidding signals extracted from the input object, if found.
187      *
188      * @throws JSONException if the key is found but the schema is incorrect
189      * @throws NullPointerException if the key found by the field is null
190      * @throws IllegalArgumentException if the extracted signals fail data validation
191      */
192     @Nullable
getDailyUpdateUriFromJsonObject()193     public Uri getDailyUpdateUriFromJsonObject()
194             throws JSONException, NullPointerException, IllegalArgumentException {
195         if (mResponseObject.has(DAILY_UPDATE_URI_KEY)) {
196             sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, DAILY_UPDATE_URI_KEY);
197 
198             String uri =
199                     JsonUtils.getStringFromJson(
200                             mResponseObject,
201                             DAILY_UPDATE_URI_KEY,
202                             String.format(
203                                     STRING_ERROR_FORMAT, DAILY_UPDATE_URI_KEY, mResponseHash));
204             Uri parsedUri = Uri.parse(uri);
205 
206             // TODO(b/282018172): Validate daily_update_uri field
207             // sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, DAILY_UPDATE_URI_KEY);
208             return parsedUri;
209         } else {
210             sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, DAILY_UPDATE_URI_KEY);
211             return null;
212         }
213     }
214 
215     /**
216      * Returns the user bidding signals extracted from the input object, if found.
217      *
218      * @throws JSONException if the key is found but the schema is incorrect
219      * @throws NullPointerException if the key found by the field is null
220      * @throws IllegalArgumentException if the extracted signals fail data validation
221      */
222     @Nullable
getBiddingLogicUriFromJsonObject()223     public Uri getBiddingLogicUriFromJsonObject()
224             throws JSONException, NullPointerException, IllegalArgumentException {
225         if (mResponseObject.has(BIDDING_LOGIC_URI_KEY)) {
226             sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, BIDDING_LOGIC_URI_KEY);
227 
228             String uri =
229                     JsonUtils.getStringFromJson(
230                             mResponseObject,
231                             BIDDING_LOGIC_URI_KEY,
232                             String.format(
233                                     STRING_ERROR_FORMAT, BIDDING_LOGIC_URI_KEY, mResponseHash));
234             Uri parsedUri = Uri.parse(uri);
235 
236             // TODO(b/282018172): Validate bidding_logic_uri field
237             // sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, BIDDING_LOGIC_URI_KEY);
238             return parsedUri;
239         } else {
240             sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, BIDDING_LOGIC_URI_KEY);
241             return null;
242         }
243     }
244 
245     /**
246      * Returns the user bidding signals extracted from the input object, if found.
247      *
248      * @throws JSONException if the key is found but the schema is incorrect
249      * @throws NullPointerException if the key found by the field is null
250      * @throws IllegalArgumentException if the extracted signals fail data validation
251      */
252     @Nullable
getUserBiddingSignalsFromJsonObject()253     public AdSelectionSignals getUserBiddingSignalsFromJsonObject()
254             throws JSONException, NullPointerException, IllegalArgumentException {
255         return mCustomAudienceUpdatableDataReader.getUserBiddingSignalsFromJsonObject();
256     }
257 
258     /**
259      * Returns the trusted bidding data extracted from the input object, if found.
260      *
261      * @throws JSONException if the key is found but the schema is incorrect
262      * @throws NullPointerException if the key found by the field is null
263      * @throws IllegalArgumentException if the extracted data fails data validation
264      */
265     @Nullable
getTrustedBiddingDataFromJsonObject()266     public DBTrustedBiddingData getTrustedBiddingDataFromJsonObject()
267             throws JSONException, NullPointerException, IllegalArgumentException {
268         return mCustomAudienceUpdatableDataReader.getTrustedBiddingDataFromJsonObject();
269     }
270 
271     /**
272      * Returns the list of ads extracted from the input object, if found.
273      *
274      * @throws JSONException if the key is found but the schema is incorrect
275      * @throws NullPointerException if the key found by the field is null
276      * @throws IllegalArgumentException if the extracted ads fail data validation
277      */
278     @Nullable
getAdsFromJsonObject()279     public List<DBAdData> getAdsFromJsonObject()
280             throws JSONException, NullPointerException, IllegalArgumentException {
281         return mCustomAudienceUpdatableDataReader.getAdsFromJsonObject();
282     }
283 }
284