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 android.adservices.common;
18 
19 import android.annotation.NonNull;
20 import android.os.Parcel;
21 import android.os.Parcelable;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 import com.android.internal.util.Preconditions;
25 
26 import org.json.JSONException;
27 import org.json.JSONObject;
28 
29 import java.time.Duration;
30 import java.util.Objects;
31 
32 /**
33  * A frequency cap for a specific ad counter key.
34  *
35  * <p>Frequency caps define the maximum rate an event can occur within a given time interval. If the
36  * frequency cap is exceeded, the associated ad will be filtered out of ad selection.
37  */
38 public final class KeyedFrequencyCap implements Parcelable {
39     /** @hide */
40     @VisibleForTesting public static final String AD_COUNTER_KEY_FIELD_NAME = "ad_counter_key";
41     /** @hide */
42     @VisibleForTesting public static final String MAX_COUNT_FIELD_NAME = "max_count";
43     /** @hide */
44     @VisibleForTesting public static final String INTERVAL_FIELD_NAME = "interval_in_seconds";
45 
46     /** @hide */
47     public static final String MAX_COUNT_NOT_POSITIVE_ERROR_MESSAGE =
48             "KeyedFrequencyCap max count %d must be strictly positive";
49     /** @hide */
50     public static final String INTERVAL_NULL_ERROR_MESSAGE =
51             "KeyedFrequencyCap interval must not be null";
52     /** @hide */
53     public static final String INTERVAL_NOT_POSITIVE_FORMAT =
54             "KeyedFrequencyCap interval %s must be strictly positive";
55     /** @hide */
56     public static final String MAX_INTERVAL_EXCEEDED_FORMAT =
57             "KeyedFrequencyCap interval %s must be no greater than %s";
58     /** @hide */
59     public static final Duration MAX_INTERVAL = Duration.ofDays(100);
60 
61     // 4 bytes for the key, 12 bytes for the duration, and 4 for the maxCount
62     private static final int SIZE_OF_FIXED_FIELDS = 20;
63 
64     private final int mAdCounterKey;
65     private final int mMaxCount;
66     @NonNull private final Duration mInterval;
67 
68     @NonNull
69     public static final Creator<KeyedFrequencyCap> CREATOR =
70             new Creator<KeyedFrequencyCap>() {
71                 @Override
72                 public KeyedFrequencyCap createFromParcel(@NonNull Parcel in) {
73                     Objects.requireNonNull(in);
74                     return new KeyedFrequencyCap(in);
75                 }
76 
77                 @Override
78                 public KeyedFrequencyCap[] newArray(int size) {
79                     return new KeyedFrequencyCap[size];
80                 }
81             };
82 
KeyedFrequencyCap(@onNull Builder builder)83     private KeyedFrequencyCap(@NonNull Builder builder) {
84         Objects.requireNonNull(builder);
85 
86         mAdCounterKey = builder.mAdCounterKey;
87         mMaxCount = builder.mMaxCount;
88         mInterval = builder.mInterval;
89     }
90 
KeyedFrequencyCap(@onNull Parcel in)91     private KeyedFrequencyCap(@NonNull Parcel in) {
92         Objects.requireNonNull(in);
93 
94         mAdCounterKey = in.readInt();
95         mMaxCount = in.readInt();
96         mInterval = Duration.ofSeconds(in.readLong());
97     }
98 
99     /**
100      * Returns the ad counter key that the frequency cap is applied to.
101      *
102      * <p>The ad counter key is defined by an adtech and is an arbitrary numeric identifier which
103      * defines any criteria which may have previously been counted and persisted on the device. If
104      * the on-device count exceeds the maximum count within a certain time interval, the frequency
105      * cap has been exceeded.
106      */
107     @NonNull
getAdCounterKey()108     public int getAdCounterKey() {
109         return mAdCounterKey;
110     }
111 
112     /**
113      * Returns the maximum count of event occurrences allowed within a given time interval.
114      *
115      * <p>If there are more events matching the ad counter key and ad event type counted on the
116      * device within the time interval defined by {@link #getInterval()}, the frequency cap has been
117      * exceeded, and the ad will not be eligible for ad selection.
118      *
119      * <p>For example, an ad that specifies a filter for a max count of two within one hour will not
120      * be eligible for ad selection if the event has been counted two or more times within the hour
121      * preceding the ad selection process.
122      */
getMaxCount()123     public int getMaxCount() {
124         return mMaxCount;
125     }
126 
127     /**
128      * Returns the interval, as a {@link Duration} which will be truncated to the nearest second,
129      * over which the frequency cap is calculated.
130      *
131      * <p>When this frequency cap is computed, the number of persisted events is counted in the most
132      * recent time interval. If the count of previously occurring matching events for an adtech is
133      * greater than the number returned by {@link #getMaxCount()}, the frequency cap has been
134      * exceeded, and the ad will not be eligible for ad selection.
135      */
136     @NonNull
getInterval()137     public Duration getInterval() {
138         return mInterval;
139     }
140 
141     /**
142      * @return The estimated size of this object, in bytes.
143      * @hide
144      */
getSizeInBytes()145     public int getSizeInBytes() {
146         return SIZE_OF_FIXED_FIELDS;
147     }
148 
149     /**
150      * A JSON serializer.
151      *
152      * @return A JSON serialization of this object.
153      * @hide
154      */
toJson()155     public JSONObject toJson() throws JSONException {
156         JSONObject toReturn = new JSONObject();
157         toReturn.put(AD_COUNTER_KEY_FIELD_NAME, mAdCounterKey);
158         toReturn.put(MAX_COUNT_FIELD_NAME, mMaxCount);
159         toReturn.put(INTERVAL_FIELD_NAME, mInterval.getSeconds());
160         return toReturn;
161     }
162 
163     /**
164      * A JSON de-serializer.
165      *
166      * @param json A JSON representation of an {@link KeyedFrequencyCap} object as would be
167      *     generated by {@link #toJson()}.
168      * @return An {@link KeyedFrequencyCap} object generated from the given JSON.
169      * @hide
170      */
fromJson(JSONObject json)171     public static KeyedFrequencyCap fromJson(JSONObject json) throws JSONException {
172         return new Builder(
173                         json.getInt(AD_COUNTER_KEY_FIELD_NAME),
174                         json.getInt(MAX_COUNT_FIELD_NAME),
175                         Duration.ofSeconds(json.getLong(INTERVAL_FIELD_NAME)))
176                 .build();
177     }
178 
179     @Override
writeToParcel(@onNull Parcel dest, int flags)180     public void writeToParcel(@NonNull Parcel dest, int flags) {
181         Objects.requireNonNull(dest);
182         dest.writeInt(mAdCounterKey);
183         dest.writeInt(mMaxCount);
184         dest.writeLong(mInterval.getSeconds());
185     }
186 
187     /** @hide */
188     @Override
describeContents()189     public int describeContents() {
190         return 0;
191     }
192 
193     /** Checks whether the {@link KeyedFrequencyCap} objects contain the same information. */
194     @Override
equals(Object o)195     public boolean equals(Object o) {
196         if (this == o) return true;
197         if (!(o instanceof KeyedFrequencyCap)) return false;
198         KeyedFrequencyCap that = (KeyedFrequencyCap) o;
199         return mMaxCount == that.mMaxCount
200                 && mInterval.equals(that.mInterval)
201                 && mAdCounterKey == that.mAdCounterKey;
202     }
203 
204     /** Returns the hash of the {@link KeyedFrequencyCap} object's data. */
205     @Override
hashCode()206     public int hashCode() {
207         return Objects.hash(mAdCounterKey, mMaxCount, mInterval);
208     }
209 
210     @Override
toString()211     public String toString() {
212         return "KeyedFrequencyCap{"
213                 + "mAdCounterKey="
214                 + mAdCounterKey
215                 + ", mMaxCount="
216                 + mMaxCount
217                 + ", mInterval="
218                 + mInterval
219                 + '}';
220     }
221 
222     /** Builder for creating {@link KeyedFrequencyCap} objects. */
223     public static final class Builder {
224         private int mAdCounterKey;
225         private int mMaxCount;
226         @NonNull private Duration mInterval;
227 
Builder(int adCounterKey, int maxCount, @NonNull Duration interval)228         public Builder(int adCounterKey, int maxCount, @NonNull Duration interval) {
229             Preconditions.checkArgument(
230                     maxCount > 0, MAX_COUNT_NOT_POSITIVE_ERROR_MESSAGE, maxCount);
231             Objects.requireNonNull(interval, INTERVAL_NULL_ERROR_MESSAGE);
232             Preconditions.checkArgument(
233                     interval.getSeconds() > 0, INTERVAL_NOT_POSITIVE_FORMAT, interval);
234             Preconditions.checkArgument(
235                     interval.getSeconds() <= MAX_INTERVAL.getSeconds(),
236                     MAX_INTERVAL_EXCEEDED_FORMAT,
237                     interval,
238                     MAX_INTERVAL);
239 
240             mAdCounterKey = adCounterKey;
241             mMaxCount = maxCount;
242             mInterval = interval;
243         }
244 
245         /**
246          * Sets the ad counter key the frequency cap applies to.
247          *
248          * <p>See {@link #getAdCounterKey()} for more information.
249          */
250         @NonNull
setAdCounterKey(int adCounterKey)251         public Builder setAdCounterKey(int adCounterKey) {
252             mAdCounterKey = adCounterKey;
253             return this;
254         }
255 
256         /**
257          * Sets the maximum count within the time interval for the frequency cap.
258          *
259          * <p>See {@link #getMaxCount()} for more information.
260          */
261         @NonNull
setMaxCount(int maxCount)262         public Builder setMaxCount(int maxCount) {
263             Preconditions.checkArgument(
264                     maxCount > 0, MAX_COUNT_NOT_POSITIVE_ERROR_MESSAGE, maxCount);
265             mMaxCount = maxCount;
266             return this;
267         }
268 
269         /**
270          * Sets the interval, as a {@link Duration} which will be truncated to the nearest second,
271          * over which the frequency cap is calculated.
272          *
273          * <p>See {@link #getInterval()} for more information.
274          */
275         @NonNull
setInterval(@onNull Duration interval)276         public Builder setInterval(@NonNull Duration interval) {
277             Objects.requireNonNull(interval, INTERVAL_NULL_ERROR_MESSAGE);
278             Preconditions.checkArgument(
279                     interval.getSeconds() > 0, INTERVAL_NOT_POSITIVE_FORMAT, interval);
280             Preconditions.checkArgument(
281                     interval.getSeconds() <= MAX_INTERVAL.getSeconds(),
282                     MAX_INTERVAL_EXCEEDED_FORMAT,
283                     interval,
284                     MAX_INTERVAL);
285             mInterval = interval;
286             return this;
287         }
288 
289         /** Builds and returns a {@link KeyedFrequencyCap} instance. */
290         @NonNull
build()291         public KeyedFrequencyCap build() {
292             return new KeyedFrequencyCap(this);
293         }
294     }
295 }
296