blob: 4c5b280ddb3afda286675c963cc8c87e4b3854ca [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 static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
import android.adservices.adselection.AdSelectionConfig;
import android.adservices.common.AdTechIdentifier;
import android.adservices.exceptions.AdServicesException;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.util.Pair;
import androidx.annotation.RequiresApi;
import com.android.adservices.LoggerFactory;
import com.android.adservices.data.adselection.AdSelectionEntryDao;
import com.android.adservices.data.adselection.DBAdSelection;
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.AdSelectionServiceFilter;
import com.android.adservices.service.common.BinderFlagReader;
import com.android.adservices.service.common.FrequencyCapAdDataValidator;
import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
import com.android.adservices.service.devapi.CustomAudienceDevOverridesHelper;
import com.android.adservices.service.devapi.DevContext;
import com.android.adservices.service.stats.AdSelectionExecutionLogger;
import com.android.adservices.service.stats.AdServicesLogger;
import com.android.adservices.service.stats.AdServicesLoggerUtil;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.stream.Collectors;
/** Orchestrate on-device ad selection. */
// TODO(b/269798827): Enable for R.
@RequiresApi(Build.VERSION_CODES.S)
public class OnDeviceAdSelectionRunner extends AdSelectionRunner {
private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
@NonNull protected final AdsScoreGenerator mAdsScoreGenerator;
@NonNull protected final AdServicesHttpsClient mAdServicesHttpsClient;
@NonNull protected final PerBuyerBiddingRunner mPerBuyerBiddingRunner;
@NonNull protected final AdFilterer mAdFilterer;
@NonNull protected final AdCounterKeyCopier mAdCounterKeyCopier;
public OnDeviceAdSelectionRunner(
@NonNull final Context context,
@NonNull final CustomAudienceDao customAudienceDao,
@NonNull final AdSelectionEntryDao adSelectionEntryDao,
@NonNull final AdServicesHttpsClient adServicesHttpsClient,
@NonNull final ExecutorService lightweightExecutorService,
@NonNull final ExecutorService backgroundExecutorService,
@NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
@NonNull final AdServicesLogger adServicesLogger,
@NonNull final DevContext devContext,
@NonNull final Flags flags,
@NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger,
@NonNull final AdSelectionServiceFilter adSelectionServiceFilter,
@NonNull final AdFilterer adFilterer,
@NonNull final AdCounterKeyCopier adCounterKeyCopier,
@NonNull final AdCounterHistogramUpdater adCounterHistogramUpdater,
@NonNull final FrequencyCapAdDataValidator frequencyCapAdDataValidator,
@NonNull final DebugReporting debugReporting,
final int callerUid,
boolean shouldUseUnifiedTables) {
super(
context,
customAudienceDao,
adSelectionEntryDao,
lightweightExecutorService,
backgroundExecutorService,
scheduledExecutor,
adServicesLogger,
flags,
adSelectionExecutionLogger,
adSelectionServiceFilter,
adFilterer,
frequencyCapAdDataValidator,
adCounterHistogramUpdater,
debugReporting,
callerUid,
shouldUseUnifiedTables);
Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(adFilterer);
Objects.requireNonNull(adCounterKeyCopier);
mAdServicesHttpsClient = adServicesHttpsClient;
mAdFilterer = adFilterer;
mAdCounterKeyCopier = adCounterKeyCopier;
boolean cpcBillingEnabled = BinderFlagReader.readFlag(mFlags::getFledgeCpcBillingEnabled);
boolean dataVersionHeaderEnabled =
BinderFlagReader.readFlag(mFlags::getFledgeDataVersionHeaderEnabled);
mAdsScoreGenerator =
new AdsScoreGeneratorImpl(
new AdSelectionScriptEngine(
context,
() -> flags.getEnforceIsolateMaxHeapSize(),
() -> flags.getIsolateMaxHeapSizeBytes(),
mAdCounterKeyCopier,
mDebugReporting.getScriptStrategy(),
cpcBillingEnabled),
mLightweightExecutorService,
mBackgroundExecutorService,
mScheduledExecutor,
mAdServicesHttpsClient,
devContext,
mAdSelectionEntryDao,
flags,
adSelectionExecutionLogger,
mDebugReporting,
dataVersionHeaderEnabled);
mPerBuyerBiddingRunner =
new PerBuyerBiddingRunner(
new AdBidGeneratorImpl(
context,
mAdServicesHttpsClient,
mLightweightExecutorService,
mBackgroundExecutorService,
mScheduledExecutor,
devContext,
mCustomAudienceDao,
mAdCounterKeyCopier,
flags,
mDebugReporting,
cpcBillingEnabled,
dataVersionHeaderEnabled),
new TrustedBiddingDataFetcher(
adServicesHttpsClient,
devContext,
new CustomAudienceDevOverridesHelper(devContext, customAudienceDao),
lightweightExecutorService,
dataVersionHeaderEnabled),
mScheduledExecutor,
mBackgroundExecutorService,
flags);
}
@VisibleForTesting
OnDeviceAdSelectionRunner(
@NonNull final Context context,
@NonNull final CustomAudienceDao customAudienceDao,
@NonNull final AdSelectionEntryDao adSelectionEntryDao,
@NonNull final AdServicesHttpsClient adServicesHttpsClient,
@NonNull final ExecutorService lightweightExecutorService,
@NonNull final ExecutorService backgroundExecutorService,
@NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
@NonNull final AdsScoreGenerator adsScoreGenerator,
@NonNull final AdSelectionIdGenerator adSelectionIdGenerator,
@NonNull Clock clock,
@NonNull final AdServicesLogger adServicesLogger,
@NonNull final Flags flags,
int callerUid,
@NonNull final AdSelectionServiceFilter adSelectionServiceFilter,
@NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger,
@NonNull final PerBuyerBiddingRunner perBuyerBiddingRunner,
@NonNull final AdFilterer adFilterer,
@NonNull final AdCounterKeyCopier adCounterKeyCopier,
@NonNull final AdCounterHistogramUpdater adCounterHistogramUpdater,
@NonNull final FrequencyCapAdDataValidator frequencyCapAdDataValidator,
@NonNull final DebugReporting debugReporting,
boolean shouldUseUnifiedTables) {
super(
context,
customAudienceDao,
adSelectionEntryDao,
lightweightExecutorService,
backgroundExecutorService,
scheduledExecutor,
adSelectionIdGenerator,
clock,
adServicesLogger,
flags,
callerUid,
adSelectionServiceFilter,
adFilterer,
frequencyCapAdDataValidator,
adCounterHistogramUpdater,
adSelectionExecutionLogger,
debugReporting,
shouldUseUnifiedTables);
Objects.requireNonNull(adsScoreGenerator);
Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(adFilterer);
Objects.requireNonNull(adCounterKeyCopier);
mAdsScoreGenerator = adsScoreGenerator;
mAdServicesHttpsClient = adServicesHttpsClient;
mPerBuyerBiddingRunner = perBuyerBiddingRunner;
mAdFilterer = adFilterer;
mAdCounterKeyCopier = adCounterKeyCopier;
}
/**
* Orchestrate on device ad selection.
*
* @param adSelectionConfig Set of data from Sellers and Buyers needed for Ad Auction and
* Selection
*/
public ListenableFuture<AdSelectionOrchestrationResult> orchestrateAdSelection(
@NonNull final AdSelectionConfig adSelectionConfig,
@NonNull final String callerPackageName,
ListenableFuture<List<DBCustomAudience>> buyerCustomAudience) {
ListenableFuture<List<DBCustomAudience>> filteredCas =
FluentFuture.from(buyerCustomAudience)
.transform(mAdFilterer::filterCustomAudiences, mLightweightExecutorService);
AsyncFunction<List<DBCustomAudience>, List<AdBiddingOutcome>> bidAds =
buyerCAs -> runAdBidding(buyerCAs, adSelectionConfig);
ListenableFuture<List<AdBiddingOutcome>> biddingOutcome =
Futures.transformAsync(filteredCas, bidAds, mLightweightExecutorService);
AsyncFunction<List<AdBiddingOutcome>, Pair<List<AdScoringOutcome>, List<DebugReport>>>
mapBidsToScores = bids -> runAdScoring(bids, adSelectionConfig);
ListenableFuture<Pair<List<AdScoringOutcome>, List<DebugReport>>>
scoredAdsAndBuyerDebugReports =
Futures.transformAsync(
biddingOutcome, mapBidsToScores, mLightweightExecutorService);
Function<
Pair<List<AdScoringOutcome>, List<DebugReport>>,
Pair<AdScoringOutcome, AdSelectionContext>>
reduceScoresToWinner = this::getWinningOutcomeAndContext;
ListenableFuture<Pair<AdScoringOutcome, AdSelectionContext>> winningOutcomeAndDebugReports =
Futures.transform(
scoredAdsAndBuyerDebugReports,
reduceScoresToWinner,
mLightweightExecutorService);
AsyncFunction<Pair<AdScoringOutcome, AdSelectionContext>, AdSelectionOrchestrationResult>
mapWinnerToDBResult = this::createAdSelectionResult;
ListenableFuture<AdSelectionOrchestrationResult> dbAdSelectionBuilder =
Futures.transformAsync(
winningOutcomeAndDebugReports,
mapWinnerToDBResult,
mLightweightExecutorService);
// Clean up after the future is complete, out of critical path
dbAdSelectionBuilder.addListener(this::cleanUpCache, mLightweightExecutorService);
return dbAdSelectionBuilder;
}
private ListenableFuture<List<AdBiddingOutcome>> runAdBidding(
@NonNull final List<DBCustomAudience> customAudiences,
@NonNull final AdSelectionConfig adSelectionConfig) {
try {
if (customAudiences.isEmpty()) {
sLogger.w("Need not invoke bidding on empty list of CAs");
// Return empty list of bids
return Futures.immediateFuture(Collections.EMPTY_LIST);
}
mAdSelectionExecutionLogger.startRunAdBidding(customAudiences);
Map<AdTechIdentifier, List<DBCustomAudience>> buyerToCustomAudienceMap =
mapBuyerToCustomAudience(customAudiences);
sLogger.v("Invoking bidding for #%d buyers", buyerToCustomAudienceMap.size());
long perBuyerBiddingTimeoutMs = mFlags.getAdSelectionBiddingTimeoutPerBuyerMs();
return FluentFuture.from(
Futures.successfulAsList(
buyerToCustomAudienceMap.entrySet().parallelStream()
.map(
entry -> {
return mPerBuyerBiddingRunner.runBidding(
entry.getKey(),
entry.getValue(),
perBuyerBiddingTimeoutMs,
adSelectionConfig);
})
.flatMap(List::stream)
.collect(Collectors.toList())))
.transform(this::endSuccessfulBidding, mLightweightExecutorService)
.catching(
RuntimeException.class,
this::endFailedBiddingWithRuntimeException,
mLightweightExecutorService);
} catch (Exception e) {
mAdSelectionExecutionLogger.endBiddingProcess(
null, AdServicesLoggerUtil.getResultCodeFromException(e));
throw e;
}
}
@NonNull
private List<AdBiddingOutcome> endSuccessfulBidding(@NonNull List<AdBiddingOutcome> result) {
Objects.requireNonNull(result);
mAdSelectionExecutionLogger.endBiddingProcess(result, STATUS_SUCCESS);
return result;
}
private void endSilentFailedBidding(RuntimeException e) {
mAdSelectionExecutionLogger.endBiddingProcess(
null, AdServicesLoggerUtil.getResultCodeFromException(e));
}
@Nullable
private List<AdBiddingOutcome> endFailedBiddingWithRuntimeException(RuntimeException e) {
mAdSelectionExecutionLogger.endBiddingProcess(
null, AdServicesLoggerUtil.getResultCodeFromException(e));
throw e;
}
@SuppressLint("DefaultLocale")
private ListenableFuture<Pair<List<AdScoringOutcome>, List<DebugReport>>> runAdScoring(
@NonNull final List<AdBiddingOutcome> adBiddingOutcomes,
@NonNull final AdSelectionConfig adSelectionConfig)
throws AdServicesException {
sLogger.v("Got %d total bidding outcomes", adBiddingOutcomes.size());
List<AdBiddingOutcome> validBiddingOutcomes =
adBiddingOutcomes.stream().filter(Objects::nonNull).collect(Collectors.toList());
sLogger.v("Got %d valid bidding outcomes", validBiddingOutcomes.size());
if (validBiddingOutcomes.isEmpty()
&& adSelectionConfig.getBuyerSignedContextualAds().isEmpty()) {
sLogger.w("Received empty list of successful bidding outcomes and contextual ads");
throw new IllegalStateException(ERROR_NO_VALID_BIDS_OR_CONTEXTUAL_ADS_FOR_SCORING);
}
List<DebugReport> buyerDebugReports = new ArrayList<>();
if (mDebugReporting.isEnabled()) {
buyerDebugReports.addAll(
validBiddingOutcomes.stream()
.map(AdBiddingOutcome::getDebugReport)
.filter(Objects::nonNull)
.collect(Collectors.toList()));
}
return mAdsScoreGenerator
.runAdScoring(validBiddingOutcomes, adSelectionConfig)
.transform(
adScoringOutcomes -> Pair.create(adScoringOutcomes, buyerDebugReports),
mLightweightExecutorService);
}
private Pair<AdScoringOutcome, AdSelectionContext> getWinningOutcomeAndContext(
@NonNull
Pair<List<AdScoringOutcome>, List<DebugReport>>
scoringOutcomesAndBuyerDebugReports) {
sLogger.v("Scoring completed, generating winning outcome");
// Iterate over the list to find the winner and second highest scored ad outcome.
AdScoringOutcome winningAd = null;
AdScoringOutcome secondHighestScoredAd = null;
double winningAdScore = Double.MIN_VALUE;
double secondHighestAdScore = Double.MIN_VALUE;
List<DebugReport> debugReports =
new ArrayList<>(scoringOutcomesAndBuyerDebugReports.second);
for (AdScoringOutcome adScoringOutcome : scoringOutcomesAndBuyerDebugReports.first) {
if (mDebugReporting.isEnabled() && Objects.nonNull(adScoringOutcome.getDebugReport())) {
debugReports.add(adScoringOutcome.getDebugReport());
}
double score = adScoringOutcome.getAdWithScore().getScore();
if (score <= 0) {
continue;
}
if (score > winningAdScore) {
// winner becomes second highest scored ad.
secondHighestScoredAd = winningAd;
secondHighestAdScore = winningAdScore;
winningAd = adScoringOutcome;
winningAdScore = score;
} else if (score > secondHighestAdScore) {
secondHighestScoredAd = adScoringOutcome;
}
}
if (Objects.isNull(winningAd)) {
throw new IllegalStateException(ERROR_NO_WINNING_AD_FOUND);
}
AdSelectionContext adSelectionContext;
if (mDebugReporting.isEnabled()) {
adSelectionContext =
new AdSelectionContext(debugReports, winningAd, secondHighestScoredAd);
} else {
adSelectionContext =
new AdSelectionContext(
/*debugReports*/ Collections.emptyList(),
/*winningAdScoringOutcome*/ null,
/*secondHighestAdScoringOutcome*/ null);
}
return Pair.create(winningAd, adSelectionContext);
}
/**
* This method populates an Ad Selection result ready to be persisted in DB, with all the fields
* except adSelectionId and creation time, which should be created as close as possible to
* persistence logic
*
* @param winningAdScoringOutcomeAndContext Winning Ad for overall Ad Selection with
* AdSelectionContext
* @return A {@link Pair} with a Builder for {@link DBAdSelection} populated with necessary data
* and a string containing the JS with the decision logic from this buyer.
*/
@VisibleForTesting
ListenableFuture<AdSelectionOrchestrationResult> createAdSelectionResult(
@NonNull Pair<AdScoringOutcome, AdSelectionContext> winningAdScoringOutcomeAndContext) {
DBAdSelection.Builder dbAdSelectionBuilder = new DBAdSelection.Builder();
sLogger.v("Creating Ad Selection result from scoring winner");
String buyerContextualSignalsString;
String sellerContextualSignalsString;
// There should always be a winner.
AdScoringOutcome scoringWinner = winningAdScoringOutcomeAndContext.first;
if (Objects.isNull(scoringWinner.getBuyerContextualSignals())) {
buyerContextualSignalsString = "{}";
} else {
buyerContextualSignalsString = scoringWinner.getBuyerContextualSignals().toString();
}
if (Objects.isNull(scoringWinner.getSellerContextualSignals())) {
sellerContextualSignalsString = "{}";
} else {
sellerContextualSignalsString = scoringWinner.getSellerContextualSignals().toString();
}
dbAdSelectionBuilder
.setWinningAdBid(scoringWinner.getAdWithScore().getAdWithBid().getBid())
.setCustomAudienceSignals(scoringWinner.getCustomAudienceSignals())
.setWinningAdRenderUri(
scoringWinner.getAdWithScore().getAdWithBid().getAdData().getRenderUri())
.setBiddingLogicUri(scoringWinner.getBiddingLogicUri())
.setBuyerContextualSignals(buyerContextualSignalsString)
.setSellerContextualSignals(sellerContextualSignalsString);
// TODO(b/230569187): get the contextualSignal securely = "invoking app name"
final DBAdSelection.Builder copiedDBAdSelectionBuilder =
mAdCounterKeyCopier.copyAdCounterKeys(dbAdSelectionBuilder, scoringWinner);
return getWinnerBiddingLogicJs(scoringWinner)
.transform(
biddingLogicJs ->
new AdSelectionOrchestrationResult(
copiedDBAdSelectionBuilder,
biddingLogicJs,
winningAdScoringOutcomeAndContext.second.mDebugReportList,
winningAdScoringOutcomeAndContext
.second
.mWinnerAdScoringOutcome,
winningAdScoringOutcomeAndContext
.second
.mSecondHighestAdScoringOutcome),
mLightweightExecutorService);
}
@VisibleForTesting
FluentFuture<String> getWinnerBiddingLogicJs(AdScoringOutcome scoringOutcome) {
String biddingLogicJs =
(scoringOutcome.isBiddingLogicJsDownloaded())
? scoringOutcome.getBiddingLogicJs()
: "";
return FluentFuture.from(Futures.immediateFuture(biddingLogicJs));
}
private Map<AdTechIdentifier, List<DBCustomAudience>> mapBuyerToCustomAudience(
final List<DBCustomAudience> customAudienceList) {
Map<AdTechIdentifier, List<DBCustomAudience>> buyerToCustomAudienceMap = new HashMap<>();
for (DBCustomAudience customAudience : customAudienceList) {
buyerToCustomAudienceMap
.computeIfAbsent(customAudience.getBuyer(), k -> new ArrayList<>())
.add(customAudience);
}
sLogger.v("Created mapping for #%d buyers", buyerToCustomAudienceMap.size());
return buyerToCustomAudienceMap;
}
/**
* Given we no longer need to fetch data from web for this run of Ad Selection, we attempt to
* clean up cache.
*/
private void cleanUpCache() {
mAdServicesHttpsClient.getAssociatedCache().cleanUp();
}
private static class AdSelectionContext {
@NonNull final List<DebugReport> mDebugReportList;
@Nullable final AdScoringOutcome mWinnerAdScoringOutcome;
@Nullable final AdScoringOutcome mSecondHighestAdScoringOutcome;
AdSelectionContext(
@NonNull List<DebugReport> debugReports,
@Nullable AdScoringOutcome winningAdScoringOutcome,
@Nullable AdScoringOutcome secondHighestAdScoringOutcome) {
this.mDebugReportList = debugReports;
this.mWinnerAdScoringOutcome = winningAdScoringOutcome;
this.mSecondHighestAdScoringOutcome = secondHighestAdScoringOutcome;
}
}
}