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