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