blob: 95e17928315dea0760f09e7295f9fe5e270006a1 [file] [log] [blame]
/*
* Copyright (C) 2023 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 static com.android.adservices.service.common.Throttler.ApiKey.FLEDGE_API_FETCH_CUSTOM_AUDIENCE;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN;
import android.adservices.common.AdServicesStatusUtils;
import android.adservices.common.AdTechIdentifier;
import android.adservices.common.FledgeErrorResponse;
import android.adservices.customaudience.FetchAndJoinCustomAudienceCallback;
import android.adservices.customaudience.FetchAndJoinCustomAudienceInput;
import android.annotation.NonNull;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import androidx.annotation.RequiresApi;
import com.android.adservices.LoggerFactory;
import com.android.adservices.data.customaudience.CustomAudienceDao;
import com.android.adservices.data.customaudience.DBCustomAudience;
import com.android.adservices.service.Flags;
import com.android.adservices.service.common.AdTechIdentifierValidator;
import com.android.adservices.service.common.CallingAppUidSupplier;
import com.android.adservices.service.common.CustomAudienceServiceFilter;
import com.android.adservices.service.common.httpclient.AdServicesHttpClientRequest;
import com.android.adservices.service.common.httpclient.AdServicesHttpClientResponse;
import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
import com.android.adservices.service.consent.ConsentManager;
import com.android.adservices.service.exception.FilterException;
import com.android.adservices.service.stats.AdServicesLogger;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.json.JSONException;
import org.json.JSONObject;
import java.time.Clock;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
/** Implementation of Fetch Custom Audience. */
// TODO(b/269798827): Enable for R.
@RequiresApi(Build.VERSION_CODES.S)
public class FetchCustomAudienceImpl {
private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
private static final int API_NAME = AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN;
private static final String CUSTOM_AUDIENCE_HEADER = "X-CUSTOM-AUDIENCE-DATA";
@NonNull private final AdServicesLogger mAdServicesLogger;
@NonNull private final ListeningExecutorService mExecutorService;
@NonNull private final CallingAppUidSupplier mCallingAppUidSupplier;
@NonNull private final ConsentManager mConsentManager;
@NonNull private final CustomAudienceServiceFilter mCustomAudienceServiceFilter;
@NonNull private final AdServicesHttpsClient mHttpClient;
@NonNull private final Clock mClock;
@NonNull private final CustomAudienceDao mCustomAudienceDao;
@NonNull private final boolean mFledgeFetchCustomAudienceEnabled;
@NonNull private final boolean mEnforceForegroundStatusForFledgeCustomAudience;
@NonNull private final int mMaxUserBiddingSignalsSizeB;
@NonNull private final int mMaxTrustedBiddingDataSizeB;
@NonNull private final int mFledgeCustomAudienceMaxAdsSizeB;
@NonNull private final int mFledgeCustomAudienceMaxNumAds;
@NonNull private final boolean mFledgeAdSelectionFilteringEnabled;
@NonNull private AdTechIdentifier mBuyer;
@VisibleForTesting
public FetchCustomAudienceImpl(
@NonNull Flags flags,
@NonNull Clock clock,
@NonNull AdServicesLogger adServicesLogger,
@NonNull ExecutorService executor,
@NonNull CustomAudienceDao customAudienceDao,
@NonNull CallingAppUidSupplier callingAppUidSupplier,
@NonNull ConsentManager consentManager,
@NonNull CustomAudienceServiceFilter customAudienceServiceFilter,
@NonNull AdServicesHttpsClient httpClient) {
Objects.requireNonNull(flags);
Objects.requireNonNull(clock);
Objects.requireNonNull(adServicesLogger);
Objects.requireNonNull(executor);
Objects.requireNonNull(customAudienceDao);
Objects.requireNonNull(callingAppUidSupplier);
Objects.requireNonNull(consentManager);
Objects.requireNonNull(customAudienceServiceFilter);
Objects.requireNonNull(httpClient);
mClock = clock;
mAdServicesLogger = adServicesLogger;
mExecutorService = MoreExecutors.listeningDecorator(executor);
mCustomAudienceDao = customAudienceDao;
mCallingAppUidSupplier = callingAppUidSupplier;
mConsentManager = consentManager;
mCustomAudienceServiceFilter = customAudienceServiceFilter;
mHttpClient = httpClient;
// TODO(b/278016820): Revisit handling field limit validation.
// Ensuring process-stable flag values by assigning to local variables at instantiation.
mFledgeFetchCustomAudienceEnabled = flags.getFledgeFetchCustomAudienceEnabled();
mEnforceForegroundStatusForFledgeCustomAudience =
flags.getEnforceForegroundStatusForFledgeCustomAudience();
mMaxUserBiddingSignalsSizeB = flags.getFledgeCustomAudienceMaxUserBiddingSignalsSizeB();
mMaxTrustedBiddingDataSizeB = flags.getFledgeCustomAudienceMaxTrustedBiddingDataSizeB();
mFledgeCustomAudienceMaxAdsSizeB = flags.getFledgeCustomAudienceMaxAdsSizeB();
mFledgeCustomAudienceMaxNumAds = flags.getFledgeCustomAudienceMaxNumAds();
mFledgeAdSelectionFilteringEnabled = flags.getFledgeAdSelectionFilteringEnabled();
}
/** Adds a user to a fetched custom audience. */
public void doFetchCustomAudience(
@NonNull FetchAndJoinCustomAudienceInput input,
@NonNull FetchAndJoinCustomAudienceCallback callback) {
try {
// Failing fast and silently if fetchCustomAudience is disabled.
if (!mFledgeFetchCustomAudienceEnabled) {
sLogger.v("fetchCustomAudience is disabled.");
throw new IllegalStateException("fetchCustomAudience is disabled.");
} else {
sLogger.v("fetchCustomAudience is enabled.");
// TODO(b/282017342): Evaluate correctness of futures chain.
FluentFuture.from(mExecutorService.submit(() -> filterAndValidateRequest(input)))
.transformAsync(ignoreVoid -> performFetch(input), mExecutorService)
.transformAsync(
httpResponse -> validateResponse(input, httpResponse),
mExecutorService)
.addCallback(
new FutureCallback<Void>() {
@Override
public void onSuccess(Void unusedResult) {
sLogger.v("Completed fetchCustomAudience execution");
notifySuccess(callback);
}
@Override
public void onFailure(Throwable t) {
sLogger.d(
t,
"Error encountered in fetchCustomAudience"
+ " execution");
if (t instanceof FilterException
&& t.getCause()
instanceof
ConsentManager.RevokedConsentException) {
// Skip logging if a FilterException occurs.
// AdSelectionServiceFilter ensures the failing
// assertion is logged
// internally.
// Fail Silently by notifying success to caller
notifySuccess(callback);
} else {
notifyFailure(callback, t);
}
}
},
mExecutorService);
}
} catch (Throwable t) {
notifyFailure(callback, t);
}
}
private Void filterAndValidateRequest(@NonNull FetchAndJoinCustomAudienceInput input) {
sLogger.v("In fetchCustomAudience filterAndValidateRequest");
// Extract buyer Ad Tech identifier
// TODO(b/282017511): Use enrollment data to store host eTLD+1.
String host = input.getFetchUri().getHost();
AdTechIdentifierValidator adTechIdentifierValidator =
new AdTechIdentifierValidator("FetchCustomAudienceImpl", "buyer");
adTechIdentifierValidator.validate(host);
mBuyer = AdTechIdentifier.fromString(host);
// Filter request
try {
mCustomAudienceServiceFilter.filterRequest(
mBuyer,
input.getCallerPackageName(),
mEnforceForegroundStatusForFledgeCustomAudience,
true,
mCallingAppUidSupplier.getCallingAppUid(),
API_NAME,
FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
} catch (Throwable t) {
throw new FilterException(t);
}
// Validate request
// TODO(b/282017511): Add implementation.
sLogger.v("Completed fetchCustomAudience filterAndValidateRequest");
return null;
}
private ListenableFuture<AdServicesHttpClientResponse> performFetch(
@NonNull FetchAndJoinCustomAudienceInput input) throws JSONException {
sLogger.v("In fetchCustomAudience performFetch");
// Optional fields as a json string.
JSONObject json = new JSONObject();
if (input.getName() != null) {
json.put(FetchCustomAudienceReader.NAME_KEY, input.getName());
}
if (input.getActivationTime() != null) {
json.put(FetchCustomAudienceReader.ACTIVATION_TIME_KEY, input.getActivationTime());
}
if (input.getExpirationTime() != null) {
json.put(FetchCustomAudienceReader.EXPIRATION_TIME_KEY, input.getExpirationTime());
}
if (input.getUserBiddingSignals() != null) {
json.put(
CustomAudienceUpdatableDataReader.USER_BIDDING_SIGNALS_KEY,
input.getUserBiddingSignals());
}
String jsonString = json.toString();
// Custom headers under X-CUSTOM-AUDIENCE-DATA
ImmutableMap<String, String> requestProperties =
ImmutableMap.of(CUSTOM_AUDIENCE_HEADER, jsonString);
// TODO(b/282017511): Validate size of headers.
sLogger.v("Sending request from fetchCustomAudience performFetch");
// GET request
return mHttpClient.fetchPayload(
AdServicesHttpClientRequest.builder()
.setRequestProperties(requestProperties)
.setUri(input.getFetchUri())
.build());
}
private ListenableFuture<Void> validateResponse(
@NonNull FetchAndJoinCustomAudienceInput input,
@NonNull AdServicesHttpClientResponse fetchResponse) {
return FluentFuture.from(
mExecutorService.submit(
() -> {
// Parse + Validate response
String responseJsonString = fetchResponse.getResponseBody();
JSONObject responseJson = new JSONObject(responseJsonString);
FetchCustomAudienceReader reader =
new FetchCustomAudienceReader(
responseJson,
String.valueOf(fetchResponse.hashCode()),
mBuyer,
mMaxUserBiddingSignalsSizeB,
mMaxTrustedBiddingDataSizeB,
mFledgeCustomAudienceMaxAdsSizeB,
mFledgeCustomAudienceMaxNumAds,
mFledgeAdSelectionFilteringEnabled);
Uri dailyUpdateUri = reader.getDailyUpdateUriFromJsonObject();
// TODO(b/282018172): Validate partial input CA + partial response CA =
// full CA
// TODO(b/286146443): Document how fields from the server are used.
// Add response from server
DBCustomAudience.Builder customAudienceBuilder =
new DBCustomAudience.Builder()
.setName(reader.getNameFromJsonObject())
.setBuyer(mBuyer)
.setActivationTime(
reader.getActivationTimeFromJsonObject())
.setExpirationTime(
reader.getExpirationTimeFromJsonObject())
.setBiddingLogicUri(
reader.getBiddingLogicUriFromJsonObject())
.setTrustedBiddingData(
reader.getTrustedBiddingDataFromJsonObject())
.setAds(reader.getAdsFromJsonObject())
.setUserBiddingSignals(
reader.getUserBiddingSignalsFromJsonObject());
// Override response from server with input fields
if (input.getName() != null) {
customAudienceBuilder =
customAudienceBuilder.setName(input.getName());
}
if (input.getActivationTime() != null) {
customAudienceBuilder =
customAudienceBuilder.setActivationTime(
input.getActivationTime());
}
if (input.getExpirationTime() != null) {
customAudienceBuilder =
customAudienceBuilder.setExpirationTime(
input.getExpirationTime());
}
if (input.getUserBiddingSignals() != null) {
customAudienceBuilder =
customAudienceBuilder.setUserBiddingSignals(
input.getUserBiddingSignals());
}
customAudienceBuilder.setOwner(input.getCallerPackageName());
Instant currentTime = mClock.instant();
customAudienceBuilder.setCreationTime(currentTime);
customAudienceBuilder.setLastAdsAndBiddingDataUpdatedTime(currentTime);
DBCustomAudience customAudience = customAudienceBuilder.build();
// Persist response
mCustomAudienceDao.insertOrOverwriteCustomAudience(
customAudience, dailyUpdateUri);
return null;
}));
}
// TODO(b/283857101): Move DB handling to persistResponse using a common CustomAudienceReader.
// private Void persistResponse (@NonNull DBCustomAudience customAudience) {}
private void notifyFailure(FetchAndJoinCustomAudienceCallback callback, Throwable t) {
try {
int resultCode;
boolean isFilterException = t instanceof FilterException;
if (isFilterException) {
resultCode = FilterException.getResultCode(t);
} else if (t instanceof IllegalArgumentException) {
resultCode = AdServicesStatusUtils.STATUS_INVALID_ARGUMENT;
} else {
sLogger.d(t, "Unexpected error during operation");
resultCode = AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
}
// Skip logging if a FilterException occurs.
// AdSelectionServiceFilter ensures the failing assertion is logged internally.
// Note: Failure is logged before the callback to ensure deterministic testing.
if (!isFilterException) {
mAdServicesLogger.logFledgeApiCallStats(API_NAME, resultCode, 0);
}
callback.onFailure(
new FledgeErrorResponse.Builder()
.setStatusCode(resultCode)
.setErrorMessage(t.getMessage())
.build());
} catch (RemoteException e) {
sLogger.e(e, "Unable to send failed result to the callback");
mAdServicesLogger.logFledgeApiCallStats(
API_NAME, AdServicesStatusUtils.STATUS_INTERNAL_ERROR, 0);
throw new RuntimeException(e);
}
}
/** Invokes the onSuccess function from the callback and handles the exception. */
private void notifySuccess(@NonNull FetchAndJoinCustomAudienceCallback callback) {
try {
mAdServicesLogger.logFledgeApiCallStats(
API_NAME, AdServicesStatusUtils.STATUS_SUCCESS, 0);
callback.onSuccess();
} catch (RemoteException e) {
sLogger.e(e, "Unable to send successful result to the callback");
mAdServicesLogger.logFledgeApiCallStats(
API_NAME, AdServicesStatusUtils.STATUS_INTERNAL_ERROR, 0);
throw new RuntimeException(e);
}
}
}