/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.adservices.customaudience; import android.adservices.adselection.GetAdSelectionDataRequest; import android.adservices.common.AdData; import android.adservices.common.AdSelectionSignals; import android.adservices.common.AdTechIdentifier; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.net.Uri; import android.os.OutcomeReceiver; import android.os.Parcel; import android.os.Parcelable; import com.android.adservices.AdServicesParcelableUtil; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.Instant; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; /** * Represents the information necessary for a custom audience to participate in ad selection. * *

A custom audience is an abstract grouping of users with similar demonstrated interests. This * class is a collection of some data stored on a device that is necessary to serve advertisements * targeting a single custom audience. */ public final class CustomAudience implements Parcelable { /** @hide */ public static final int FLAG_AUCTION_SERVER_REQUEST_DEFAULT = 0; /** * This auction server request flag indicates to the service that ads for this {@link * CustomAudience} can be omitted in the server auction payload. */ @FlaggedApi( "com.android.adservices.flags.fledge_custom_audience_auction_server_request_flags_enabled") public static final int FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS = 1 << 0; @NonNull private final AdTechIdentifier mBuyer; @NonNull private final String mName; @Nullable private final Instant mActivationTime; @Nullable private final Instant mExpirationTime; @NonNull private final Uri mDailyUpdateUri; @Nullable private final AdSelectionSignals mUserBiddingSignals; @Nullable private final TrustedBiddingData mTrustedBiddingData; @NonNull private final Uri mBiddingLogicUri; @NonNull private final List mAds; @AuctionServerRequestFlag private final int mAuctionServerRequestFlags; /** @hide */ @IntDef( flag = true, prefix = {"FLAG_AUCTION_SERVER_REQUEST"}, value = {FLAG_AUCTION_SERVER_REQUEST_DEFAULT, FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS}) @Retention(RetentionPolicy.SOURCE) public @interface AuctionServerRequestFlag {} @NonNull public static final Creator CREATOR = new Creator() { @Override public CustomAudience createFromParcel(@NonNull Parcel in) { Objects.requireNonNull(in); return new CustomAudience(in); } @Override public CustomAudience[] newArray(int size) { return new CustomAudience[size]; } }; private CustomAudience(@NonNull CustomAudience.Builder builder) { Objects.requireNonNull(builder); mBuyer = builder.mBuyer; mName = builder.mName; mActivationTime = builder.mActivationTime; mExpirationTime = builder.mExpirationTime; mDailyUpdateUri = builder.mDailyUpdateUri; mUserBiddingSignals = builder.mUserBiddingSignals; mTrustedBiddingData = builder.mTrustedBiddingData; mBiddingLogicUri = builder.mBiddingLogicUri; mAds = builder.mAds; mAuctionServerRequestFlags = builder.mAuctionServerRequestFlags; } private CustomAudience(@NonNull Parcel in) { Objects.requireNonNull(in); mBuyer = AdTechIdentifier.CREATOR.createFromParcel(in); mName = in.readString(); mActivationTime = AdServicesParcelableUtil.readNullableFromParcel( in, (sourceParcel) -> Instant.ofEpochMilli(sourceParcel.readLong())); mExpirationTime = AdServicesParcelableUtil.readNullableFromParcel( in, (sourceParcel) -> Instant.ofEpochMilli(sourceParcel.readLong())); mDailyUpdateUri = Uri.CREATOR.createFromParcel(in); mUserBiddingSignals = AdServicesParcelableUtil.readNullableFromParcel( in, AdSelectionSignals.CREATOR::createFromParcel); mTrustedBiddingData = AdServicesParcelableUtil.readNullableFromParcel( in, TrustedBiddingData.CREATOR::createFromParcel); mBiddingLogicUri = Uri.CREATOR.createFromParcel(in); mAds = in.createTypedArrayList(AdData.CREATOR); mAuctionServerRequestFlags = in.readInt(); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { Objects.requireNonNull(dest); mBuyer.writeToParcel(dest, flags); dest.writeString(mName); AdServicesParcelableUtil.writeNullableToParcel( dest, mActivationTime, (targetParcel, sourceInstant) -> targetParcel.writeLong(sourceInstant.toEpochMilli())); AdServicesParcelableUtil.writeNullableToParcel( dest, mExpirationTime, (targetParcel, sourceInstant) -> targetParcel.writeLong(sourceInstant.toEpochMilli())); mDailyUpdateUri.writeToParcel(dest, flags); AdServicesParcelableUtil.writeNullableToParcel( dest, mUserBiddingSignals, (targetParcel, sourceSignals) -> sourceSignals.writeToParcel(targetParcel, flags)); AdServicesParcelableUtil.writeNullableToParcel( dest, mTrustedBiddingData, (targetParcel, sourceData) -> sourceData.writeToParcel(targetParcel, flags)); mBiddingLogicUri.writeToParcel(dest, flags); dest.writeTypedList(mAds); dest.writeInt(mAuctionServerRequestFlags); } @Override public String toString() { return "CustomAudience{" + "mBuyer=" + mBuyer + ", mName='" + mName + ", mActivationTime=" + mActivationTime + ", mExpirationTime=" + mExpirationTime + ", mDailyUpdateUri=" + mDailyUpdateUri + ", mUserBiddingSignals=" + mUserBiddingSignals + ", mTrustedBiddingData=" + mTrustedBiddingData + ", mBiddingLogicUri=" + mBiddingLogicUri + ", mAds=" + mAds + ", mAuctionServerRequestFlags=" + mAuctionServerRequestFlags + '}'; } /** @hide */ @Override public int describeContents() { return 0; } /** * A buyer is identified by a domain in the form "buyerexample.com". * * @return an {@link AdTechIdentifier} containing the custom audience's buyer's domain */ @NonNull public AdTechIdentifier getBuyer() { return mBuyer; } /** * The custom audience's name is an arbitrary string provided by the owner and buyer on creation * of the {@link CustomAudience} object. * *

The overall size of the CA is limited and the size of this field is considered using * {@link String#getBytes()} in {@code UTF-8} encoding. * * @return the String name of the custom audience */ @NonNull public String getName() { return mName; } /** * On creation of the {@link CustomAudience} object, an optional activation time may be set in * the future, in order to serve a delayed activation. If the field is not set, the {@link * CustomAudience} will be activated at the time of joining. * *

For example, a custom audience for lapsed users may not activate until a threshold of * inactivity is reached, at which point the custom audience's ads will participate in the ad * selection process, potentially redirecting lapsed users to the original owner application. * *

The maximum delay in activation is 60 days from initial creation. * *

If specified, the activation time must be an earlier instant than the expiration time. * * @return the timestamp {@link Instant}, truncated to milliseconds, after which the custom * audience is active */ @Nullable public Instant getActivationTime() { return mActivationTime; } /** * Once the expiration time has passed, a custom audience is no longer eligible for daily * ad/bidding data updates or participation in the ad selection process. The custom audience * will then be deleted from memory by the next daily update. * *

If no expiration time is provided on creation of the {@link CustomAudience}, expiry will * default to 60 days from activation. * *

The maximum expiry is 60 days from initial activation. * * @return the timestamp {@link Instant}, truncated to milliseconds, after which the custom * audience should be removed */ @Nullable public Instant getExpirationTime() { return mExpirationTime; } /** * This URI points to a buyer-operated server that hosts updated bidding data and ads metadata * to be used in the on-device ad selection process. The URI must use HTTPS. * * @return the custom audience's daily update URI */ @NonNull public Uri getDailyUpdateUri() { return mDailyUpdateUri; } /** * User bidding signals are optionally provided by buyers to be consumed by buyer-provided * JavaScript during ad selection in an isolated execution environment. * *

If the user bidding signals are not a valid JSON object that can be consumed by the * buyer's JS, the custom audience will not be eligible for ad selection. * *

If not specified, the {@link CustomAudience} will not participate in ad selection until * user bidding signals are provided via the daily update for the custom audience. * * @return an {@link AdSelectionSignals} object representing the user bidding signals for the * custom audience */ @Nullable public AdSelectionSignals getUserBiddingSignals() { return mUserBiddingSignals; } /** * Trusted bidding data consists of a URI pointing to a trusted server for buyers' bidding data * and a list of keys to query the server with. Note that the keys are arbitrary identifiers * that will only be used to query the trusted server for a buyer's bidding logic during ad * selection. * *

If not specified, the {@link CustomAudience} will not participate in ad selection until * trusted bidding data are provided via the daily update for the custom audience. * * @return a {@link TrustedBiddingData} object containing the custom audience's trusted bidding * data */ @Nullable public TrustedBiddingData getTrustedBiddingData() { return mTrustedBiddingData; } /** * Returns the target URI used to fetch bidding logic when a custom audience participates in the * ad selection process. The URI must use HTTPS. * * @return the URI for fetching buyer bidding logic */ @NonNull public Uri getBiddingLogicUri() { return mBiddingLogicUri; } /** * This list of {@link AdData} objects is a full and complete list of the ads that will be * served by this {@link CustomAudience} during the ad selection process. * *

If not specified, or if an empty list is provided, the {@link CustomAudience} will not * participate in ad selection until a valid list of ads are provided via the daily update for * the custom audience. * *

The combined ads size of the CA is limited and the sizes of each ad's string fields are * considered using {@link String#getBytes()} in {@code UTF-8} encoding. * * @return a {@link List} of {@link AdData} objects representing ads currently served by the * custom audience */ @NonNull public List getAds() { return mAds; } /** * Returns the bitfield of auction server request flags. These are flags that influence the * creation of the payload generated by the {@link * android.adservices.adselection.AdSelectionManager#getAdSelectionData(GetAdSelectionDataRequest, * Executor, OutcomeReceiver)} API. * *

To create this bitfield, place an {@code |} bitwise operator between each {@link * AuctionServerRequestFlag} to be enabled. */ @FlaggedApi( "com.android.adservices.flags.fledge_custom_audience_auction_server_request_flags_enabled") @AuctionServerRequestFlag public int getAuctionServerRequestFlags() { return mAuctionServerRequestFlags; } /** * Checks whether two {@link CustomAudience} objects contain the same information. */ @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof CustomAudience)) return false; CustomAudience that = (CustomAudience) o; return mBuyer.equals(that.mBuyer) && mName.equals(that.mName) && Objects.equals(mActivationTime, that.mActivationTime) && Objects.equals(mExpirationTime, that.mExpirationTime) && mDailyUpdateUri.equals(that.mDailyUpdateUri) && Objects.equals(mUserBiddingSignals, that.mUserBiddingSignals) && Objects.equals(mTrustedBiddingData, that.mTrustedBiddingData) && mBiddingLogicUri.equals(that.mBiddingLogicUri) && mAds.equals(that.mAds) && mAuctionServerRequestFlags == that.mAuctionServerRequestFlags; } /** * Returns the hash of the {@link CustomAudience} object's data. */ @Override public int hashCode() { return Objects.hash( mBuyer, mName, mActivationTime, mExpirationTime, mDailyUpdateUri, mUserBiddingSignals, mTrustedBiddingData, mBiddingLogicUri, mAds, mAuctionServerRequestFlags); } /** Builder for {@link CustomAudience} objects. */ public static final class Builder { @Nullable private AdTechIdentifier mBuyer; @Nullable private String mName; @Nullable private Instant mActivationTime; @Nullable private Instant mExpirationTime; @Nullable private Uri mDailyUpdateUri; @Nullable private AdSelectionSignals mUserBiddingSignals; @Nullable private TrustedBiddingData mTrustedBiddingData; @Nullable private Uri mBiddingLogicUri; @Nullable private List mAds; @AuctionServerRequestFlag private int mAuctionServerRequestFlags; // TODO(b/232883403): We may need to add @NonNUll members as args. public Builder() { } /** * Sets the buyer {@link AdTechIdentifier}. * *

See {@link #getBuyer()} for more information. */ @NonNull public CustomAudience.Builder setBuyer(@NonNull AdTechIdentifier buyer) { Objects.requireNonNull(buyer); mBuyer = buyer; return this; } /** * Sets the {@link CustomAudience} object's name. *

* See {@link #getName()} for more information. */ @NonNull public CustomAudience.Builder setName(@NonNull String name) { Objects.requireNonNull(name); mName = name; return this; } /** * Sets the time, truncated to milliseconds, after which the {@link CustomAudience} will * serve ads. * *

Set to {@code null} in order for this {@link CustomAudience} to be immediately active * and participate in ad selection. * *

See {@link #getActivationTime()} for more information. */ @NonNull public CustomAudience.Builder setActivationTime(@Nullable Instant activationTime) { mActivationTime = activationTime; return this; } /** * Sets the time, truncated to milliseconds, after which the {@link CustomAudience} should * be removed. *

* See {@link #getExpirationTime()} for more information. */ @NonNull public CustomAudience.Builder setExpirationTime(@Nullable Instant expirationTime) { mExpirationTime = expirationTime; return this; } /** * Sets the daily update URI. The URI must use HTTPS. * *

See {@link #getDailyUpdateUri()} for more information. */ @NonNull public CustomAudience.Builder setDailyUpdateUri(@NonNull Uri dailyUpdateUri) { Objects.requireNonNull(dailyUpdateUri); mDailyUpdateUri = dailyUpdateUri; return this; } /** * Sets the user bidding signals used in the ad selection process. * *

See {@link #getUserBiddingSignals()} for more information. */ @NonNull public CustomAudience.Builder setUserBiddingSignals( @Nullable AdSelectionSignals userBiddingSignals) { mUserBiddingSignals = userBiddingSignals; return this; } /** * Sets the trusted bidding data to be queried and used in the ad selection process. *

* See {@link #getTrustedBiddingData()} for more information. */ @NonNull public CustomAudience.Builder setTrustedBiddingData( @Nullable TrustedBiddingData trustedBiddingData) { mTrustedBiddingData = trustedBiddingData; return this; } /** * Sets the URI to fetch bidding logic from for use in the ad selection process. The URI * must use HTTPS. * *

See {@link #getBiddingLogicUri()} for more information. */ @NonNull public CustomAudience.Builder setBiddingLogicUri(@NonNull Uri biddingLogicUri) { Objects.requireNonNull(biddingLogicUri); mBiddingLogicUri = biddingLogicUri; return this; } /** * Sets the initial remarketing ads served by the custom audience. Will be assigned with an * empty list if not provided. * *

See {@link #getAds()} for more information. */ @NonNull public CustomAudience.Builder setAds(@Nullable List ads) { mAds = ads; return this; } /** * Sets the bitfield of auction server request flags. * *

See {@link #getAuctionServerRequestFlags()} for more information. */ @FlaggedApi( "com.android.adservices.flags.fledge_custom_audience_auction_server_request_flags_enabled") @NonNull public CustomAudience.Builder setAuctionServerRequestFlags( @AuctionServerRequestFlag int auctionServerRequestFlags) { mAuctionServerRequestFlags = auctionServerRequestFlags; return this; } /** * Builds an instance of a {@link CustomAudience}. * * @throws NullPointerException if any non-null parameter is null * @throws IllegalArgumentException if the expiration time occurs before activation time * @throws IllegalArgumentException if the expiration time is set before the current time */ @NonNull public CustomAudience build() { Objects.requireNonNull(mBuyer, "The buyer has not been provided"); Objects.requireNonNull(mName, "The name has not been provided"); Objects.requireNonNull(mDailyUpdateUri, "The daily update URI has not been provided"); Objects.requireNonNull(mBiddingLogicUri, "The bidding logic URI has not been provided"); // To pass the API lint, we should not allow null Collection. if (mAds == null) { mAds = List.of(); } return new CustomAudience(this); } } }