blob: 0cfb4a31b97c9e29e533437a5689a94123009900 [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.adselection;
import android.adservices.adselection.AdSelectionConfig;
import android.adservices.adselection.AdWithBid;
import android.adservices.common.AdData;
import android.adservices.common.AdSelectionSignals;
import android.adservices.common.AdTechIdentifier;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.Uri;
import android.util.Pair;
import com.android.adservices.LogUtil;
import com.android.adservices.data.adselection.CustomAudienceSignals;
import com.android.adservices.data.customaudience.CustomAudienceDao;
import com.android.adservices.data.customaudience.DBCustomAudience;
import com.android.adservices.data.customaudience.DBTrustedBiddingData;
import com.android.adservices.service.Flags;
import com.android.adservices.service.common.AdServicesHttpsClient;
import com.android.adservices.service.devapi.CustomAudienceDevOverridesHelper;
import com.android.adservices.service.devapi.DevContext;
import com.android.adservices.service.js.IsolateSettings;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.UncheckedTimeoutException;
import org.json.JSONException;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
/**
* This class implements the ad bid generator. A new instance is assumed to be created for every
* call
*/
public class AdBidGeneratorImpl implements AdBidGenerator {
@VisibleForTesting static final String QUERY_PARAM_KEYS = "keys";
@VisibleForTesting
static final String MISSING_TRUSTED_BIDDING_SIGNALS = "Error fetching trusted bidding signals";
@VisibleForTesting
static final String MISSING_BIDDING_LOGIC = "Error fetching bidding js logic";
@VisibleForTesting
static final String BIDDING_TIMED_OUT = "Bidding exceeded allowed time limit";
@VisibleForTesting
static final String BIDDING_ENCOUNTERED_UNEXPECTED_ERROR =
"Bidding failed for unexpected error";
@NonNull private final Context mContext;
@NonNull private final ListeningExecutorService mLightweightExecutorService;
@NonNull private final ListeningExecutorService mBackgroundExecutorService;
@NonNull private final AdSelectionScriptEngine mAdSelectionScriptEngine;
@NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
@NonNull private final CustomAudienceDevOverridesHelper mCustomAudienceDevOverridesHelper;
@NonNull private final Flags mFlags;
public AdBidGeneratorImpl(
@NonNull Context context,
@NonNull AdServicesHttpsClient adServicesHttpsClient,
@NonNull ListeningExecutorService lightweightExecutorService,
@NonNull ListeningExecutorService backgroundExecutorService,
@NonNull DevContext devContext,
@NonNull CustomAudienceDao customAudienceDao,
@NonNull Flags flags) {
Objects.requireNonNull(context);
Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(lightweightExecutorService);
Objects.requireNonNull(backgroundExecutorService);
Objects.requireNonNull(devContext);
Objects.requireNonNull(customAudienceDao);
Objects.requireNonNull(flags);
mContext = context;
mLightweightExecutorService = lightweightExecutorService;
mBackgroundExecutorService = backgroundExecutorService;
mAdServicesHttpsClient = adServicesHttpsClient;
mCustomAudienceDevOverridesHelper =
new CustomAudienceDevOverridesHelper(devContext, customAudienceDao);
mFlags = flags;
mAdSelectionScriptEngine =
new AdSelectionScriptEngine(
mContext,
() -> mFlags.getEnforceIsolateMaxHeapSize(),
() -> mFlags.getIsolateMaxHeapSizeBytes());
}
@VisibleForTesting
AdBidGeneratorImpl(
@NonNull Context context,
@NonNull ListeningExecutorService lightWeightExecutorService,
@NonNull ListeningExecutorService backgroundExecutorService,
@NonNull AdSelectionScriptEngine adSelectionScriptEngine,
@NonNull AdServicesHttpsClient adServicesHttpsClient,
@NonNull CustomAudienceDevOverridesHelper customAudienceDevOverridesHelper,
@NonNull Flags flags,
@NonNull IsolateSettings isolateSettings) {
Objects.requireNonNull(context);
Objects.requireNonNull(lightWeightExecutorService);
Objects.requireNonNull(backgroundExecutorService);
Objects.requireNonNull(adSelectionScriptEngine);
Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(customAudienceDevOverridesHelper);
Objects.requireNonNull(flags);
Objects.requireNonNull(isolateSettings);
mContext = context;
mLightweightExecutorService = lightWeightExecutorService;
mBackgroundExecutorService = backgroundExecutorService;
mAdSelectionScriptEngine = adSelectionScriptEngine;
mAdServicesHttpsClient = adServicesHttpsClient;
mCustomAudienceDevOverridesHelper = customAudienceDevOverridesHelper;
mFlags = flags;
}
@Override
@NonNull
public FluentFuture<AdBiddingOutcome> runAdBiddingPerCA(
@NonNull DBCustomAudience customAudience,
@NonNull AdSelectionSignals adSelectionSignals,
@NonNull AdSelectionSignals buyerSignals,
@NonNull AdSelectionSignals contextualSignals,
@NonNull AdSelectionConfig adSelectionConfig) {
Objects.requireNonNull(customAudience);
Objects.requireNonNull(adSelectionSignals);
Objects.requireNonNull(buyerSignals);
Objects.requireNonNull(contextualSignals);
Objects.requireNonNull(adSelectionConfig);
LogUtil.v("Running Ad Bidding for CA : %s", customAudience.getName());
if (customAudience.getAds().isEmpty()) {
LogUtil.v("No Ads found for CA: %s, skipping", customAudience.getName());
return FluentFuture.from(Futures.immediateFuture(null));
}
AdSelectionSignals userSignals = buildUserSignals(customAudience);
CustomAudienceSignals customAudienceSignals =
CustomAudienceSignals.buildFromCustomAudience(customAudience);
// TODO(b/221862406): implement ads filtering logic.
FluentFuture<String> buyerDecisionLogic =
getBuyerDecisionLogic(
customAudience.getBiddingLogicUri(),
customAudience.getOwner(),
customAudience.getBuyer(),
customAudience.getName());
FluentFuture<Pair<AdWithBid, String>> adWithBidPair =
buyerDecisionLogic.transformAsync(
decisionLogic -> {
return runBidding(
customAudience,
decisionLogic,
buyerSignals,
contextualSignals,
customAudienceSignals,
userSignals,
adSelectionSignals);
},
mLightweightExecutorService);
return adWithBidPair
.transform(
candidate -> {
if (Objects.isNull(candidate)
|| Objects.isNull(candidate.first)
|| candidate.first.getBid() <= 0.0) {
LogUtil.v(
"Bidding for CA completed but result %s is filtered out",
candidate);
return null;
}
CustomAudienceBiddingInfo customAudienceInfo =
CustomAudienceBiddingInfo.create(
customAudience, candidate.second);
LogUtil.v(
"Creating Ad Bidding Outcome for CA: %s",
customAudience.getName());
AdBiddingOutcome result =
AdBiddingOutcome.builder()
.setAdWithBid(candidate.first)
.setCustomAudienceBiddingInfo(customAudienceInfo)
.build();
LogUtil.d("Bidding for CA %s transformed", customAudience.getName());
return result;
},
mLightweightExecutorService)
.withTimeout(
mFlags.getAdSelectionBiddingTimeoutPerCaMs(),
TimeUnit.MILLISECONDS,
// TODO(b/237103033): Comply with thread usage policy for AdServices;
// use a global scheduled executor
new ScheduledThreadPoolExecutor(1))
.catching(
JSONException.class, this::handleBiddingError, mLightweightExecutorService)
.catching(
TimeoutException.class,
this::handleTimeoutError,
mLightweightExecutorService);
}
@Nullable
private AdBiddingOutcome handleTimeoutError(TimeoutException e) {
LogUtil.e(e, "Bid Generation exceeded time limit");
// Despite this exception will be flattened, after doing `successfulAsList` on bids, keeping
// it consistent with Scoring and overall Ad Selection timeouts
throw new UncheckedTimeoutException(BIDDING_TIMED_OUT);
}
@Nullable
private AdBiddingOutcome handleBiddingError(JSONException e) {
// TODO(b/231326420): Define and implement the certain non-expected exceptions should be
// re-throw from the AdBidGenerator.
LogUtil.e(e, "Failed to generate bids for the ads in this custom audience.");
return null;
}
private FluentFuture<AdSelectionSignals> getTrustedBiddingSignals(
@NonNull DBTrustedBiddingData trustedBiddingData,
@NonNull String owner,
@NonNull AdTechIdentifier buyer,
@NonNull String name) {
Objects.requireNonNull(trustedBiddingData);
final Uri trustedBiddingUri = trustedBiddingData.getUri();
final List<String> trustedBiddingKeys = trustedBiddingData.getKeys();
final String keysQueryParams = String.join(",", trustedBiddingKeys);
final Uri trustedBiddingUriWithKeys =
Uri.parse(trustedBiddingUri.toString())
.buildUpon()
.appendQueryParameter(QUERY_PARAM_KEYS, keysQueryParams)
.build();
FluentFuture<AdSelectionSignals> trustedSignalsOverride =
FluentFuture.from(
mBackgroundExecutorService.submit(
() ->
mCustomAudienceDevOverridesHelper
.getTrustedBiddingSignalsOverride(
owner, buyer, name)));
return trustedSignalsOverride
.transformAsync(
jsOverride -> {
if (jsOverride == null) {
LogUtil.v("Fetching trusted bidding Signals from server");
return Futures.transform(
mAdServicesHttpsClient.fetchPayload(
trustedBiddingUriWithKeys),
s -> s == null ? null : AdSelectionSignals.fromString(s),
mLightweightExecutorService);
} else {
LogUtil.d(
"Developer options enabled and override trusted signals"
+ " are provided for the current Custom Audience."
+ " Skipping call to server.");
return Futures.immediateFuture(jsOverride);
}
},
mLightweightExecutorService)
.catching(
Exception.class,
e -> {
LogUtil.w(e, "Exception encountered when fetching trusted signals");
throw new IllegalStateException(MISSING_TRUSTED_BIDDING_SIGNALS);
},
mLightweightExecutorService);
}
private FluentFuture<String> getBuyerDecisionLogic(
@NonNull final Uri decisionLogicUri,
@NonNull String owner,
@NonNull AdTechIdentifier buyer,
@NonNull String name) {
FluentFuture<String> jsOverrideFuture =
FluentFuture.from(
mBackgroundExecutorService.submit(
() ->
mCustomAudienceDevOverridesHelper.getBiddingLogicOverride(
owner, buyer, name)));
return jsOverrideFuture
.transformAsync(
jsOverride -> {
if (jsOverride == null) {
LogUtil.v(
"Fetching buyer decision logic from server: %s",
decisionLogicUri.toString());
return mAdServicesHttpsClient.fetchPayload(decisionLogicUri);
} else {
LogUtil.d(
"Developer options enabled and an override JS is provided "
+ "for the current Custom Audience. "
+ "Skipping call to server.");
return Futures.immediateFuture(jsOverride);
}
},
mLightweightExecutorService)
.catching(
Exception.class,
e -> {
LogUtil.w(
e, "Exception encountered when fetching buyer decision logic");
throw new IllegalStateException(MISSING_BIDDING_LOGIC);
},
mLightweightExecutorService);
}
/**
* @return user information with respect to the custom audience will be available to
* generateBid(). This could include language, demographic information, information about
* custom audience such as time in list, number of impressions, last N winning impression
* timestamp etc.
*/
@NonNull
public AdSelectionSignals buildUserSignals(@Nullable DBCustomAudience customAudience) {
// TODO: implement how to build user_signals with respect to customAudience.
LogUtil.v("Building Custom Audience User Signals %s", customAudience.getName());
return AdSelectionSignals.EMPTY;
}
/** @return the {@link AdWithBid} with the best bid per CustomAudience. */
@NonNull
@VisibleForTesting
FluentFuture<Pair<AdWithBid, String>> runBidding(
@NonNull DBCustomAudience customAudience,
@NonNull String buyerDecisionLogicJs,
@NonNull AdSelectionSignals buyerSignals,
@NonNull AdSelectionSignals contextualSignals,
@NonNull CustomAudienceSignals customAudienceSignals,
@NonNull AdSelectionSignals userSignals,
@NonNull AdSelectionSignals adSelectionSignals) {
FluentFuture<AdSelectionSignals> trustedBiddingSignals =
getTrustedBiddingSignals(
customAudience.getTrustedBiddingData(),
customAudience.getOwner(),
customAudience.getBuyer(),
customAudience.getName());
// TODO(b/231265311): update AdSelectionScriptEngine AdData class objects with DBAdData
// classes and remove this conversion.
List<AdData> ads =
customAudience.getAds().stream()
.map(
adData -> {
return new AdData(adData.getRenderUri(), adData.getMetadata());
})
.collect(Collectors.toList());
return trustedBiddingSignals
.transformAsync(
biddingSignals -> {
return mAdSelectionScriptEngine.generateBids(
buyerDecisionLogicJs,
ads,
adSelectionSignals,
buyerSignals,
biddingSignals,
contextualSignals,
userSignals,
customAudienceSignals);
},
mLightweightExecutorService)
.transform(
adWithBids -> {
return new Pair<>(
getBestAdWithBidPerCA(adWithBids), buyerDecisionLogicJs);
},
mLightweightExecutorService);
}
@Nullable
private AdWithBid getBestAdWithBidPerCA(@NonNull List<AdWithBid> adWithBids) {
if (adWithBids.size() == 0) {
LogUtil.v("No ad with bids for current CA");
return null;
}
AdWithBid maxBidCandidate =
adWithBids.stream().max(Comparator.comparingDouble(AdWithBid::getBid)).get();
LogUtil.v("Obtained #%d ads with bids for current CA", adWithBids.size());
if (maxBidCandidate.getBid() <= 0.0) {
LogUtil.v("No positive bids found, no valid bids to return");
return null;
}
LogUtil.v("Returning ad candidate with highest bid: %s", maxBidCandidate);
return maxBidCandidate;
}
}