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.measurement;
18 
19 import com.android.adservices.service.measurement.util.UnsignedLong;
20 
21 import org.json.JSONArray;
22 import org.json.JSONException;
23 import org.json.JSONObject;
24 
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Objects;
29 import java.util.stream.Collectors;
30 import java.util.stream.LongStream;
31 
32 /**
33  * A class wrapper for the trigger specification from the input argument during source registration
34  */
35 public class TriggerSpec {
36     private List<UnsignedLong> mTriggerData;
37     private Long mEventReportWindowsStart;
38     private List<Long> mEventReportWindowsEnd;
39     private SummaryOperatorType mSummaryWindowOperator;
40     private List<Long> mSummaryBuckets;
41 
42     @Override
equals(Object obj)43     public boolean equals(Object obj) {
44         if (!(obj instanceof TriggerSpec)) {
45             return false;
46         }
47         TriggerSpec t = (TriggerSpec) obj;
48         return Objects.equals(mTriggerData, t.mTriggerData)
49                 && mEventReportWindowsStart.equals(t.mEventReportWindowsStart)
50                 && mEventReportWindowsEnd.equals(t.mEventReportWindowsEnd)
51                 && mSummaryWindowOperator == t.mSummaryWindowOperator
52                 && mSummaryBuckets.equals(t.mSummaryBuckets);
53     }
54 
55     @Override
hashCode()56     public int hashCode() {
57         return Objects.hash(
58                 mTriggerData,
59                 mEventReportWindowsStart,
60                 mEventReportWindowsEnd,
61                 mSummaryWindowOperator,
62                 mSummaryBuckets);
63     }
64 
65     /**
66      * @return Trigger Data
67      */
getTriggerData()68     public List<UnsignedLong> getTriggerData() {
69         return mTriggerData;
70     }
71 
72     /**
73      * @return Event Report Windows Start
74      */
getEventReportWindowsStart()75     public Long getEventReportWindowsStart() {
76         return mEventReportWindowsStart;
77     }
78 
79     /** @return Event Report Windows End */
getEventReportWindowsEnd()80     public List<Long> getEventReportWindowsEnd() {
81         return mEventReportWindowsEnd;
82     }
83 
84     /** @return Summary Window Operator */
getSummaryWindowOperator()85     public SummaryOperatorType getSummaryWindowOperator() {
86         return mSummaryWindowOperator;
87     }
88 
89     /**
90      * @return Summary Bucket
91      */
getSummaryBuckets()92     public List<Long> getSummaryBuckets() {
93         return mSummaryBuckets;
94     }
95 
96     /**
97      * Encode the parameter to JSON
98      *
99      * @return json object encode this class
100      */
encodeJSON()101     public JSONObject encodeJSON() throws JSONException {
102         JSONObject json = new JSONObject();
103         json.put(
104                 "trigger_data",
105                 new JSONArray(
106                         mTriggerData.stream()
107                                 .map(UnsignedLong::toString)
108                                 .collect(Collectors.toList())));
109         JSONObject windows = new JSONObject();
110         windows.put("start_time", mEventReportWindowsStart);
111         windows.put("end_times", new JSONArray(mEventReportWindowsEnd));
112         json.put("event_report_windows", windows);
113         json.put(
114                 "summary_window_operator",
115                 mSummaryWindowOperator.name().toLowerCase(Locale.ENGLISH));
116         json.put("summary_buckets", new JSONArray(mSummaryBuckets));
117         return json;
118     }
119 
120     /** Util function to check if the provided list values are in strictly increasing order. */
isStrictIncreasing(List<T> list)121     public static <T extends Comparable<T>> boolean isStrictIncreasing(List<T> list) {
122         if (list.size() < 2) {
123             return true;
124         }
125         for (int i = 1; i < list.size(); i++) {
126             if (list.get(i).compareTo(list.get(i - 1)) <= 0) {
127                 return false;
128             }
129         }
130         return true;
131     }
132 
133     /** The choice of the summary operator with the reporting window */
134     public enum SummaryOperatorType {
135         COUNT,
136         VALUE_SUM
137     }
138 
getLongListFromJSON(JSONObject json, String key)139     private static List<Long> getLongListFromJSON(JSONObject json, String key)
140             throws JSONException {
141         return getLongListFromJSON(json.getJSONArray(key));
142     }
143 
144     /**
145      * Parses long JSONArray into List<Long>
146      *
147      * @param jsonArray the JSON Array
148      * @return the parsed List<Long>
149      */
getLongListFromJSON(JSONArray jsonArray)150     public static List<Long> getLongListFromJSON(JSONArray jsonArray) throws JSONException {
151         List<Long> result = new ArrayList<>();
152         for (int i = 0; i < jsonArray.length(); i++) {
153             result.add(jsonArray.getLong(i));
154         }
155         return result;
156     }
157 
getTriggerDataArrayFromJSON(JSONObject json, String key)158     private static List<UnsignedLong> getTriggerDataArrayFromJSON(JSONObject json, String key)
159             throws JSONException {
160         return getTriggerDataArrayFromJSON(json.getJSONArray(key));
161     }
162 
163     /**
164      * Parses long JSONArray into List<UnsignedLong>
165      *
166      * @param jsonArray the JSON Array
167      * @return a list of UnsignedLong
168      */
getTriggerDataArrayFromJSON(JSONArray jsonArray)169     public static List<UnsignedLong> getTriggerDataArrayFromJSON(JSONArray jsonArray)
170             throws JSONException {
171         List<UnsignedLong> result = new ArrayList<>();
172         for (int i = 0; i < jsonArray.length(); i++) {
173             result.add(new UnsignedLong(jsonArray.getString(i)));
174         }
175         return result;
176     }
177 
178     /** */
179     public static final class Builder {
180         private final TriggerSpec mBuilding;
181 
Builder(JSONObject jsonObject)182         public Builder(JSONObject jsonObject) throws JSONException, IllegalArgumentException {
183             mBuilding = new TriggerSpec();
184             mBuilding.mSummaryWindowOperator = SummaryOperatorType.COUNT;
185             mBuilding.mEventReportWindowsStart = 0L;
186             mBuilding.mSummaryBuckets = new ArrayList<>();
187             mBuilding.mEventReportWindowsEnd = new ArrayList<>();
188 
189             this.setTriggerData(
190                     getTriggerDataArrayFromJSON(
191                             jsonObject, TriggerSpecs.FlexEventReportJsonKeys.TRIGGER_DATA));
192             if (!jsonObject.isNull(TriggerSpecs.FlexEventReportJsonKeys.EVENT_REPORT_WINDOWS)) {
193                 JSONObject jsonReportWindows =
194                         jsonObject.getJSONObject(
195                                 TriggerSpecs.FlexEventReportJsonKeys.EVENT_REPORT_WINDOWS);
196                 if (!jsonReportWindows.isNull(
197                         TriggerSpecs.FlexEventReportJsonKeys.START_TIME)) {
198                     this.setEventReportWindowsStart(
199                             jsonReportWindows.getLong(
200                                     TriggerSpecs.FlexEventReportJsonKeys.START_TIME));
201                 }
202                 this.setEventReportWindowsEnd(
203                         getLongListFromJSON(
204                                 jsonReportWindows,
205                                 TriggerSpecs.FlexEventReportJsonKeys.END_TIMES));
206             }
207 
208             if (!jsonObject.isNull(
209                     TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_WINDOW_OPERATOR)) {
210                 this.setSummaryWindowOperator(
211                         SummaryOperatorType.valueOf(
212                                 jsonObject
213                                         .getString(
214                                                 TriggerSpecs.FlexEventReportJsonKeys
215                                                         .SUMMARY_WINDOW_OPERATOR)
216                                         .toUpperCase(Locale.ROOT)));
217             }
218             if (!jsonObject.isNull(TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_BUCKETS)) {
219                 this.setSummaryBuckets(
220                         getLongListFromJSON(
221                                 jsonObject,
222                                 TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_BUCKETS));
223             }
224         }
225 
Builder( JSONObject jsonObject, long defaultStart, List<Long> defaultWindowEnds, int maxEventLevelReports)226         public Builder(
227                 JSONObject jsonObject,
228                 long defaultStart,
229                 List<Long> defaultWindowEnds,
230                 int maxEventLevelReports) throws JSONException, IllegalArgumentException {
231             mBuilding = new TriggerSpec();
232             mBuilding.mSummaryWindowOperator = SummaryOperatorType.COUNT;
233             mBuilding.mEventReportWindowsStart = defaultStart;
234             mBuilding.mSummaryBuckets = new ArrayList<>();
235             mBuilding.mEventReportWindowsEnd = defaultWindowEnds;
236 
237             this.setTriggerData(
238                     getTriggerDataArrayFromJSON(
239                             jsonObject, TriggerSpecs.FlexEventReportJsonKeys.TRIGGER_DATA));
240             if (!jsonObject.isNull(TriggerSpecs.FlexEventReportJsonKeys.EVENT_REPORT_WINDOWS)) {
241                 JSONObject jsonReportWindows =
242                         jsonObject.getJSONObject(
243                                 TriggerSpecs.FlexEventReportJsonKeys.EVENT_REPORT_WINDOWS);
244                 if (!jsonReportWindows.isNull(
245                         TriggerSpecs.FlexEventReportJsonKeys.START_TIME)) {
246                     this.setEventReportWindowsStart(
247                             jsonReportWindows.getLong(
248                                     TriggerSpecs.FlexEventReportJsonKeys.START_TIME));
249                 }
250 
251                 this.setEventReportWindowsEnd(
252                         getLongListFromJSON(
253                                 jsonReportWindows,
254                                 TriggerSpecs.FlexEventReportJsonKeys.END_TIMES));
255             }
256 
257             if (!jsonObject.isNull(
258                     TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_WINDOW_OPERATOR)) {
259                 this.setSummaryWindowOperator(
260                         SummaryOperatorType.valueOf(
261                                 jsonObject
262                                         .getString(
263                                                 TriggerSpecs.FlexEventReportJsonKeys
264                                                         .SUMMARY_WINDOW_OPERATOR)
265                                         .toUpperCase(Locale.ENGLISH)));
266             }
267             if (!jsonObject.isNull(TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_BUCKETS)) {
268                 List<Long> summaryBuckets =
269                         getLongListFromJSON(
270                                 jsonObject,
271                                 TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_BUCKETS);
272                 this.setSummaryBuckets(summaryBuckets.subList(
273                         0, Math.min(summaryBuckets.size(), maxEventLevelReports)));
274             } else {
275                 this.setSummaryBuckets(
276                         LongStream.range(1, maxEventLevelReports + 1)
277                                 .boxed()
278                                 .collect(Collectors.toList()));
279             }
280         }
281 
282         /** See {@link TriggerSpec#getTriggerData()} ()}. */
setTriggerData(List<UnsignedLong> triggerData)283         public Builder setTriggerData(List<UnsignedLong> triggerData) {
284             mBuilding.mTriggerData = triggerData;
285             return this;
286         }
287 
288         /** See {@link TriggerSpec#getEventReportWindowsStart()} ()}. */
setEventReportWindowsStart(Long eventReportWindowsStart)289         public Builder setEventReportWindowsStart(Long eventReportWindowsStart) {
290             mBuilding.mEventReportWindowsStart = eventReportWindowsStart;
291             return this;
292         }
293 
294         /** See {@link TriggerSpec#getEventReportWindowsEnd()} ()}. */
setEventReportWindowsEnd(List<Long> eventReportWindowsEnd)295         public Builder setEventReportWindowsEnd(List<Long> eventReportWindowsEnd) {
296             mBuilding.mEventReportWindowsEnd = eventReportWindowsEnd;
297             return this;
298         }
299 
300         /** See {@link TriggerSpec#getSummaryWindowOperator()} ()}. */
setSummaryWindowOperator(SummaryOperatorType summaryWindowOperator)301         public Builder setSummaryWindowOperator(SummaryOperatorType summaryWindowOperator) {
302             mBuilding.mSummaryWindowOperator = summaryWindowOperator;
303             return this;
304         }
305 
306         /** See {@link TriggerSpec#getSummaryBucket()} ()}. */
setSummaryBuckets(List<Long> summaryBuckets)307         public Builder setSummaryBuckets(List<Long> summaryBuckets) {
308             mBuilding.mSummaryBuckets = summaryBuckets;
309             return this;
310         }
311 
312         /** Build the {@link TriggerSpec}. */
build()313         public TriggerSpec build() {
314             return mBuilding;
315         }
316     }
317 }
318