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