blob: a02d7e7e84a5c46da41c7729ba43bcd77c7de2c7 [file] [log] [blame]
/*
* 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 com.android.adservices.service.customaudience;
import android.adservices.common.AdSelectionSignals;
import android.adservices.common.AdTechIdentifier;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.adservices.LogUtil;
import com.android.adservices.data.common.DBAdData;
import com.android.adservices.data.customaudience.DBTrustedBiddingData;
import com.android.adservices.service.Flags;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import org.json.JSONException;
import org.json.JSONObject;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
/** This class represents the result of a daily fetch that will update a custom audience. */
@AutoValue
public abstract class CustomAudienceUpdatableData {
@VisibleForTesting
enum ReadStatus {
STATUS_UNKNOWN,
STATUS_NOT_FOUND,
STATUS_FOUND_VALID,
STATUS_FOUND_INVALID
}
private static final String INVALID_JSON_TYPE_ERROR_FORMAT =
"%s Invalid JSON type while parsing %s found in JSON response";
private static final String VALIDATION_FAILED_ERROR_FORMAT =
"%s Data validation failed while parsing %s found in JSON response";
/**
* @return the user bidding signals that were sent in the update response. If there were no
* valid user bidding signals, returns {@code null}.
*/
@Nullable
public abstract AdSelectionSignals getUserBiddingSignals();
/**
* @return trusted bidding data that was sent in the update response. If no valid trusted
* bidding data was found, returns {@code null}.
*/
@Nullable
public abstract DBTrustedBiddingData getTrustedBiddingData();
/**
* @return the list of ads that were sent in the update response. If no valid ads were sent,
* returns {@code null}.
*/
@Nullable
public abstract ImmutableList<DBAdData> getAds();
/** @return the time at which the custom audience update was attempted */
@NonNull
public abstract Instant getAttemptedUpdateTime();
/**
* @return the result type for the update attempt before {@link
* #createFromResponseString(Instant, AdTechIdentifier,
* BackgroundFetchRunner.UpdateResultType, String, Flags)} was called
*/
public abstract BackgroundFetchRunner.UpdateResultType getInitialUpdateResult();
/**
* Returns whether this object represents a successful update.
*
* <ul>
* <li>An empty response is valid, representing that the buyer does not want to update its
* custom audience.
* <li>If a response is not empty but fails to be parsed into a JSON object, it will be
* considered a failed response which does not contain a successful update.
* <li>If a response is not empty and is parsed successfully into a JSON object but does not
* contain any units of updatable data, it is considered empty (albeit full of junk) and
* valid, representing that the buyer does not want to update its custom audience.
* <li>A non-empty response that contains relevant fields but which all fail to be parsed into
* valid objects is considered a failed update. This might happen if fields are found but
* do not follow the correct schema/expected object types.
* <li>A non-empty response that is not completely invalid and which does have at least one
* successful field is considered successful.
* </ul>
*
* @return {@code true} if this object represents a successful update; otherwise, {@code false}
*/
public abstract boolean getContainsSuccessfulUpdate();
/**
* Creates a {@link CustomAudienceUpdatableData} object based on the response of a GET request
* to a custom audience's daily fetch URI.
*
* <p>Note that if a response contains extra fields in its JSON, the extra information will be
* ignored, and the validation of the response will continue as if the extra data had not been
* included. For example, if {@code trusted_bidding_data} contains an extra field {@code
* campaign_ids} (which is not considered part of the {@code trusted_bidding_data} JSON schema),
* the resulting {@link CustomAudienceUpdatableData} object will not be built with the extra
* data.
*
* <p>See {@link #getContainsSuccessfulUpdate()} for more details.
*
* @param attemptedUpdateTime the time at which the update for this custom audience was
* attempted
* @param buyer the buyer ad tech's eTLD+1
* @param initialUpdateResult the result type of the fetch attempt prior to parsing the {@code
* response}
* @param response the String response returned from querying the custom audience's daily fetch
* URI
* @param flags the {@link Flags} used to get configurable limits for validating the {@code
* response}
*/
@NonNull
public static CustomAudienceUpdatableData createFromResponseString(
@NonNull Instant attemptedUpdateTime,
@NonNull AdTechIdentifier buyer,
BackgroundFetchRunner.UpdateResultType initialUpdateResult,
@NonNull final String response,
@NonNull Flags flags) {
Objects.requireNonNull(attemptedUpdateTime);
Objects.requireNonNull(buyer);
Objects.requireNonNull(response);
Objects.requireNonNull(flags);
// Use the hash of the response string as a session identifier for logging purposes
final String responseHash = "[" + response.hashCode() + "]";
LogUtil.v("Parsing JSON response string with hash %s", responseHash);
// By default unset nullable AutoValue fields are null
CustomAudienceUpdatableData.Builder dataBuilder =
builder()
.setAttemptedUpdateTime(attemptedUpdateTime)
.setContainsSuccessfulUpdate(false)
.setInitialUpdateResult(initialUpdateResult);
// No need to continue if an error occurred upstream for this custom audience update
if (initialUpdateResult != BackgroundFetchRunner.UpdateResultType.SUCCESS) {
LogUtil.v("%s Skipping response string parsing due to upstream failure", responseHash);
dataBuilder.setContainsSuccessfulUpdate(false);
return dataBuilder.build();
}
if (response.isEmpty()) {
LogUtil.v("%s Response string was empty", responseHash);
dataBuilder.setContainsSuccessfulUpdate(true);
return dataBuilder.build();
}
JSONObject responseObject;
try {
responseObject = new JSONObject(response);
} catch (JSONException exception) {
LogUtil.e("%s Error parsing JSON response into an object", responseHash);
dataBuilder.setContainsSuccessfulUpdate(false);
return dataBuilder.build();
}
CustomAudienceUpdatableDataReader reader =
new CustomAudienceUpdatableDataReader(
responseObject,
responseHash,
buyer,
flags.getFledgeCustomAudienceMaxUserBiddingSignalsSizeB(),
flags.getFledgeCustomAudienceMaxTrustedBiddingDataSizeB(),
flags.getFledgeCustomAudienceMaxAdsSizeB(),
flags.getFledgeCustomAudienceMaxNumAds());
ReadStatus userBiddingSignalsReadStatus =
readUserBiddingSignals(reader, responseHash, dataBuilder);
ReadStatus trustedBiddingDataReadStatus =
readTrustedBiddingData(reader, responseHash, dataBuilder);
ReadStatus adsReadStatus = readAds(reader, responseHash, dataBuilder);
// If there were no useful fields found, or if there was something useful found and
// successfully updated, then this object should signal a successful update.
boolean containsSuccessfulUpdate =
(userBiddingSignalsReadStatus == ReadStatus.STATUS_FOUND_VALID
|| trustedBiddingDataReadStatus == ReadStatus.STATUS_FOUND_VALID
|| adsReadStatus == ReadStatus.STATUS_FOUND_VALID)
|| (userBiddingSignalsReadStatus == ReadStatus.STATUS_NOT_FOUND
&& trustedBiddingDataReadStatus == ReadStatus.STATUS_NOT_FOUND
&& adsReadStatus == ReadStatus.STATUS_NOT_FOUND);
LogUtil.v(
"%s Completed parsing JSON response with containsSuccessfulUpdate = %b",
responseHash, containsSuccessfulUpdate);
dataBuilder.setContainsSuccessfulUpdate(containsSuccessfulUpdate);
return dataBuilder.build();
}
@VisibleForTesting
@NonNull
static ReadStatus readUserBiddingSignals(
@NonNull CustomAudienceUpdatableDataReader reader,
@NonNull String responseHash,
@NonNull CustomAudienceUpdatableData.Builder dataBuilder) {
try {
AdSelectionSignals userBiddingSignals = reader.getUserBiddingSignalsFromJsonObject();
dataBuilder.setUserBiddingSignals(userBiddingSignals);
if (userBiddingSignals == null) {
return ReadStatus.STATUS_NOT_FOUND;
} else {
return ReadStatus.STATUS_FOUND_VALID;
}
} catch (JSONException | NullPointerException exception) {
LogUtil.e(
exception,
INVALID_JSON_TYPE_ERROR_FORMAT,
responseHash,
CustomAudienceUpdatableDataReader.USER_BIDDING_SIGNALS_KEY);
dataBuilder.setUserBiddingSignals(null);
return ReadStatus.STATUS_FOUND_INVALID;
} catch (IllegalArgumentException exception) {
LogUtil.e(
exception,
VALIDATION_FAILED_ERROR_FORMAT,
responseHash,
CustomAudienceUpdatableDataReader.USER_BIDDING_SIGNALS_KEY);
dataBuilder.setUserBiddingSignals(null);
return ReadStatus.STATUS_FOUND_INVALID;
}
}
@VisibleForTesting
@NonNull
static ReadStatus readTrustedBiddingData(
@NonNull CustomAudienceUpdatableDataReader reader,
@NonNull String responseHash,
@NonNull CustomAudienceUpdatableData.Builder dataBuilder) {
try {
DBTrustedBiddingData trustedBiddingData = reader.getTrustedBiddingDataFromJsonObject();
dataBuilder.setTrustedBiddingData(trustedBiddingData);
if (trustedBiddingData == null) {
return ReadStatus.STATUS_NOT_FOUND;
} else {
return ReadStatus.STATUS_FOUND_VALID;
}
} catch (JSONException | NullPointerException exception) {
LogUtil.e(
exception,
INVALID_JSON_TYPE_ERROR_FORMAT,
responseHash,
CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_DATA_KEY);
dataBuilder.setTrustedBiddingData(null);
return ReadStatus.STATUS_FOUND_INVALID;
} catch (IllegalArgumentException exception) {
LogUtil.e(
exception,
VALIDATION_FAILED_ERROR_FORMAT,
responseHash,
CustomAudienceUpdatableDataReader.TRUSTED_BIDDING_DATA_KEY);
dataBuilder.setTrustedBiddingData(null);
return ReadStatus.STATUS_FOUND_INVALID;
}
}
@VisibleForTesting
@NonNull
static ReadStatus readAds(
@NonNull CustomAudienceUpdatableDataReader reader,
@NonNull String responseHash,
@NonNull CustomAudienceUpdatableData.Builder dataBuilder) {
try {
List<DBAdData> ads = reader.getAdsFromJsonObject();
dataBuilder.setAds(ads);
if (ads == null) {
return ReadStatus.STATUS_NOT_FOUND;
} else {
return ReadStatus.STATUS_FOUND_VALID;
}
} catch (JSONException | NullPointerException exception) {
LogUtil.e(
exception,
INVALID_JSON_TYPE_ERROR_FORMAT,
responseHash,
CustomAudienceUpdatableDataReader.ADS_KEY);
dataBuilder.setAds(null);
return ReadStatus.STATUS_FOUND_INVALID;
} catch (IllegalArgumentException exception) {
LogUtil.e(
exception,
VALIDATION_FAILED_ERROR_FORMAT,
responseHash,
CustomAudienceUpdatableDataReader.ADS_KEY);
dataBuilder.setAds(null);
return ReadStatus.STATUS_FOUND_INVALID;
}
}
/**
* Gets a Builder to make {@link #createFromResponseString(Instant, AdTechIdentifier,
* BackgroundFetchRunner.UpdateResultType, String, Flags)} easier.
*/
@VisibleForTesting
@NonNull
public static CustomAudienceUpdatableData.Builder builder() {
return new AutoValue_CustomAudienceUpdatableData.Builder();
}
/**
* This is a hidden (visible for testing) AutoValue builder to make {@link
* #createFromResponseString(Instant, AdTechIdentifier, BackgroundFetchRunner.UpdateResultType,
* String, Flags)} easier.
*/
@VisibleForTesting
@AutoValue.Builder
public abstract static class Builder {
/** Sets the user bidding signals found in the response string. */
@NonNull
public abstract Builder setUserBiddingSignals(@Nullable AdSelectionSignals value);
/** Sets the trusted bidding data found in the response string. */
@NonNull
public abstract Builder setTrustedBiddingData(@Nullable DBTrustedBiddingData value);
/** Sets the list of ads found in the response string. */
@NonNull
public abstract Builder setAds(@Nullable List<DBAdData> value);
/** Sets the time at which the custom audience update was attempted. */
@NonNull
public abstract Builder setAttemptedUpdateTime(@NonNull Instant value);
/** Sets the result of the update prior to parsing the response string. */
@NonNull
public abstract Builder setInitialUpdateResult(
BackgroundFetchRunner.UpdateResultType value);
/**
* Sets whether the response contained a successful update.
*
* <p>See {@link #getContainsSuccessfulUpdate()} for more details.
*/
@NonNull
public abstract Builder setContainsSuccessfulUpdate(boolean value);
/**
* Builds the {@link CustomAudienceUpdatableData} object and returns it.
*
* <p>Note that AutoValue doesn't by itself do any validation, so splitting the builder with
* a manual verification is recommended. See go/autovalue/builders-howto#validate for more
* information.
*/
@NonNull
protected abstract CustomAudienceUpdatableData autoValueBuild();
/** Builds, validates, and returns the {@link CustomAudienceUpdatableData} object. */
@NonNull
public final CustomAudienceUpdatableData build() {
CustomAudienceUpdatableData updatableData = autoValueBuild();
Preconditions.checkArgument(
updatableData.getContainsSuccessfulUpdate()
|| (updatableData.getUserBiddingSignals() == null
&& updatableData.getTrustedBiddingData() == null
&& updatableData.getAds() == null),
"CustomAudienceUpdatableData should not contain non-null updatable fields if"
+ " the object does not represent a successful update");
return updatableData;
}
}
}