summaryrefslogtreecommitdiff
path: root/adservices/service-core/java/com/android/adservices/service/adselection
diff options
context:
space:
mode:
Diffstat (limited to 'adservices/service-core/java/com/android/adservices/service/adselection')
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/AdBidGenerator.java5
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/AdBidGeneratorImpl.java91
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java359
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionScriptEngine.java14
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java106
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/AdsScoreGeneratorImpl.java8
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/CustomAudienceReportingSignalsArgument.java49
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/ImpressionReporter.java86
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/JsFetcher.java96
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java331
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/PerBuyerBiddingRunner.java139
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/ReportImpressionScriptEngine.java8
-rw-r--r--adservices/service-core/java/com/android/adservices/service/adselection/TrustedServerAdSelectionRunner.java405
13 files changed, 1329 insertions, 368 deletions
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdBidGenerator.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdBidGenerator.java
index 9271cc0b73..65d67fe3fa 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/AdBidGenerator.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdBidGenerator.java
@@ -16,7 +16,6 @@
package com.android.adservices.service.adselection;
-import android.adservices.adselection.AdSelectionConfig;
import android.adservices.common.AdSelectionSignals;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -37,7 +36,6 @@ interface AdBidGenerator {
* @param contextualSignals Contextual information about the App where the Ad is being shown, Ad
* slot and size, geographic location information, the seller invoking the ad selection and
* so on.
- * @param adSelectionConfig used as the primary key in remote overrides
* @return a future contains either a {@link AdBiddingOutcome} containing the candidate ad with
* the best bid for this custom audience or null if no valid ads are available for scoring.
*/
@@ -46,6 +44,5 @@ interface AdBidGenerator {
@NonNull DBCustomAudience customAudience,
@NonNull AdSelectionSignals adSelectionSignals,
@NonNull AdSelectionSignals buyerSignals,
- @NonNull AdSelectionSignals contextualSignals,
- @NonNull AdSelectionConfig adSelectionConfig);
+ @NonNull AdSelectionSignals contextualSignals);
}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdBidGeneratorImpl.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdBidGeneratorImpl.java
index 0cfb4a31b9..4e950085f9 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/AdBidGeneratorImpl.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdBidGeneratorImpl.java
@@ -16,7 +16,6 @@
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;
@@ -60,13 +59,11 @@ import java.util.stream.Collectors;
*/
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";
+ static final String QUERY_PARAM_KEYS = "keys";
@VisibleForTesting
- static final String MISSING_BIDDING_LOGIC = "Error fetching bidding js logic";
+ static final String MISSING_TRUSTED_BIDDING_SIGNALS = "Error fetching trusted bidding signals";
@VisibleForTesting
static final String BIDDING_TIMED_OUT = "Bidding exceeded allowed time limit";
@@ -78,16 +75,19 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
@NonNull private final Context mContext;
@NonNull private final ListeningExecutorService mLightweightExecutorService;
@NonNull private final ListeningExecutorService mBackgroundExecutorService;
+ @NonNull private final ScheduledThreadPoolExecutor mScheduledExecutor;
@NonNull private final AdSelectionScriptEngine mAdSelectionScriptEngine;
@NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
@NonNull private final CustomAudienceDevOverridesHelper mCustomAudienceDevOverridesHelper;
@NonNull private final Flags mFlags;
+ @NonNull private final JsFetcher mJsFetcher;
public AdBidGeneratorImpl(
@NonNull Context context,
@NonNull AdServicesHttpsClient adServicesHttpsClient,
@NonNull ListeningExecutorService lightweightExecutorService,
@NonNull ListeningExecutorService backgroundExecutorService,
+ @NonNull ScheduledThreadPoolExecutor scheduledExecutor,
@NonNull DevContext devContext,
@NonNull CustomAudienceDao customAudienceDao,
@NonNull Flags flags) {
@@ -95,6 +95,7 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(lightweightExecutorService);
Objects.requireNonNull(backgroundExecutorService);
+ Objects.requireNonNull(scheduledExecutor);
Objects.requireNonNull(devContext);
Objects.requireNonNull(customAudienceDao);
Objects.requireNonNull(flags);
@@ -102,6 +103,7 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
mContext = context;
mLightweightExecutorService = lightweightExecutorService;
mBackgroundExecutorService = backgroundExecutorService;
+ mScheduledExecutor = scheduledExecutor;
mAdServicesHttpsClient = adServicesHttpsClient;
mCustomAudienceDevOverridesHelper =
new CustomAudienceDevOverridesHelper(devContext, customAudienceDao);
@@ -111,6 +113,12 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
mContext,
() -> mFlags.getEnforceIsolateMaxHeapSize(),
() -> mFlags.getIsolateMaxHeapSizeBytes());
+ mJsFetcher =
+ new JsFetcher(
+ backgroundExecutorService,
+ lightweightExecutorService,
+ mCustomAudienceDevOverridesHelper,
+ adServicesHttpsClient);
}
@VisibleForTesting
@@ -118,27 +126,33 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
@NonNull Context context,
@NonNull ListeningExecutorService lightWeightExecutorService,
@NonNull ListeningExecutorService backgroundExecutorService,
+ @NonNull ScheduledThreadPoolExecutor scheduledExecutor,
@NonNull AdSelectionScriptEngine adSelectionScriptEngine,
@NonNull AdServicesHttpsClient adServicesHttpsClient,
@NonNull CustomAudienceDevOverridesHelper customAudienceDevOverridesHelper,
@NonNull Flags flags,
- @NonNull IsolateSettings isolateSettings) {
+ @NonNull IsolateSettings isolateSettings,
+ @NonNull JsFetcher jsFetcher) {
Objects.requireNonNull(context);
Objects.requireNonNull(lightWeightExecutorService);
Objects.requireNonNull(backgroundExecutorService);
+ Objects.requireNonNull(scheduledExecutor);
Objects.requireNonNull(adSelectionScriptEngine);
Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(customAudienceDevOverridesHelper);
Objects.requireNonNull(flags);
Objects.requireNonNull(isolateSettings);
+ Objects.requireNonNull(jsFetcher);
mContext = context;
mLightweightExecutorService = lightWeightExecutorService;
mBackgroundExecutorService = backgroundExecutorService;
+ mScheduledExecutor = scheduledExecutor;
mAdSelectionScriptEngine = adSelectionScriptEngine;
mAdServicesHttpsClient = adServicesHttpsClient;
mCustomAudienceDevOverridesHelper = customAudienceDevOverridesHelper;
mFlags = flags;
+ mJsFetcher = jsFetcher;
}
@Override
@@ -147,13 +161,11 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
@NonNull DBCustomAudience customAudience,
@NonNull AdSelectionSignals adSelectionSignals,
@NonNull AdSelectionSignals buyerSignals,
- @NonNull AdSelectionSignals contextualSignals,
- @NonNull AdSelectionConfig adSelectionConfig) {
+ @NonNull AdSelectionSignals contextualSignals) {
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()) {
@@ -161,14 +173,13 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
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(
+ mJsFetcher.getBuyerDecisionLogic(
customAudience.getBiddingLogicUri(),
customAudience.getOwner(),
customAudience.getBuyer(),
@@ -183,7 +194,6 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
buyerSignals,
contextualSignals,
customAudienceSignals,
- userSignals,
adSelectionSignals);
},
mLightweightExecutorService);
@@ -216,9 +226,7 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
.withTimeout(
mFlags.getAdSelectionBiddingTimeoutPerCaMs(),
TimeUnit.MILLISECONDS,
- // TODO(b/237103033): Comply with thread usage policy for AdServices;
- // use a global scheduled executor
- new ScheduledThreadPoolExecutor(1))
+ mScheduledExecutor)
.catching(
JSONException.class, this::handleBiddingError, mLightweightExecutorService)
.catching(
@@ -293,59 +301,10 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
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.
+ * @return the {@link AdWithBid} with the best bid per CustomAudience.
*/
@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,
@@ -353,7 +312,6 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
@NonNull AdSelectionSignals buyerSignals,
@NonNull AdSelectionSignals contextualSignals,
@NonNull CustomAudienceSignals customAudienceSignals,
- @NonNull AdSelectionSignals userSignals,
@NonNull AdSelectionSignals adSelectionSignals) {
FluentFuture<AdSelectionSignals> trustedBiddingSignals =
getTrustedBiddingSignals(
@@ -380,7 +338,6 @@ public class AdBidGeneratorImpl implements AdBidGenerator {
buyerSignals,
biddingSignals,
contextualSignals,
- userSignals,
customAudienceSignals);
},
mLightweightExecutorService)
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java
index 2f69f2f3d9..7851ef3986 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java
@@ -23,18 +23,14 @@ import android.adservices.adselection.AdSelectionCallback;
import android.adservices.adselection.AdSelectionConfig;
import android.adservices.adselection.AdSelectionInput;
import android.adservices.adselection.AdSelectionResponse;
-import android.adservices.common.AdSelectionSignals;
import android.adservices.common.AdServicesStatusUtils;
import android.adservices.common.FledgeErrorResponse;
-import android.adservices.exceptions.AdServicesException;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.os.LimitExceededException;
import android.os.RemoteException;
-import android.util.Pair;
import com.android.adservices.LogUtil;
import com.android.adservices.data.adselection.AdSelectionEntryDao;
@@ -43,18 +39,18 @@ import com.android.adservices.data.adselection.DBBuyerDecisionLogic;
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.AdServicesHttpsClient;
import com.android.adservices.service.common.AppImportanceFilter;
import com.android.adservices.service.common.AppImportanceFilter.WrongCallingApplicationStateException;
import com.android.adservices.service.common.FledgeAllowListsFilter;
import com.android.adservices.service.common.FledgeAuthorizationFilter;
import com.android.adservices.service.common.Throttler;
import com.android.adservices.service.consent.ConsentManager;
-import com.android.adservices.service.devapi.DevContext;
+import com.android.adservices.service.js.JSSandboxIsNotAvailableException;
+import com.android.adservices.service.js.JSScriptEngine;
import com.android.adservices.service.stats.AdServicesLogger;
+import com.android.adservices.service.stats.ApiServiceLatencyCalculator;
import com.android.internal.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FluentFuture;
@@ -66,20 +62,13 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.UncheckedTimeoutException;
import java.time.Clock;
-import java.time.Instant;
import java.util.List;
import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
/**
* Orchestrator that runs the Ads Auction/Bidding and Scoring logic The class expects the caller to
@@ -88,7 +77,7 @@ import java.util.stream.Collectors;
*
* <p>Class takes in an executor on which it runs the AdSelection logic
*/
-public final class AdSelectionRunner {
+public abstract class AdSelectionRunner {
@VisibleForTesting static final String AD_SELECTION_ERROR_PATTERN = "%s: %s";
@@ -112,86 +101,90 @@ public final class AdSelectionRunner {
@VisibleForTesting
static final String AD_SELECTION_THROTTLED = "Ad selection exceeded allowed rate limit";
+ @VisibleForTesting
+ static final String JS_SANDBOX_IS_NOT_AVAILABLE =
+ String.format(
+ AD_SELECTION_ERROR_PATTERN,
+ ERROR_AD_SELECTION_FAILURE,
+ "JS Sandbox is not available");
+
public static final long DAY_IN_SECONDS = 60 * 60 * 24;
- @NonNull private final Context mContext;
- @NonNull private final CustomAudienceDao mCustomAudienceDao;
- @NonNull private final AdSelectionEntryDao mAdSelectionEntryDao;
- @NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
- @NonNull private final ListeningExecutorService mLightweightExecutorService;
- @NonNull private final ListeningExecutorService mBackgroundExecutorService;
- @NonNull private final AdsScoreGenerator mAdsScoreGenerator;
- @NonNull private final AdBidGenerator mAdBidGenerator;
- @NonNull private final AdSelectionIdGenerator mAdSelectionIdGenerator;
- @NonNull private final Clock mClock;
- @NonNull private final ConsentManager mConsentManager;
- @NonNull private final AdServicesLogger mAdServicesLogger;
- @NonNull private final Flags mFlags;
- @NonNull private final AppImportanceFilter mAppImportanceFilter;
- private final int mCallerUid;
- @NonNull private final Supplier<Throttler> mThrottlerSupplier;
- @NonNull private final FledgeAuthorizationFilter mFledgeAuthorizationFilter;
- @NonNull private final FledgeAllowListsFilter mFledgeAllowListsFilter;
+ @NonNull protected final Context mContext;
+ @NonNull protected final CustomAudienceDao mCustomAudienceDao;
+ @NonNull protected final AdSelectionEntryDao mAdSelectionEntryDao;
+ @NonNull protected final ListeningExecutorService mLightweightExecutorService;
+ @NonNull protected final ListeningExecutorService mBackgroundExecutorService;
+ @NonNull protected final ScheduledThreadPoolExecutor mScheduledExecutor;
+ @NonNull protected final AdSelectionIdGenerator mAdSelectionIdGenerator;
+ @NonNull protected final Clock mClock;
+ @NonNull protected final ConsentManager mConsentManager;
+ @NonNull protected final AdServicesLogger mAdServicesLogger;
+ @NonNull protected final Flags mFlags;
+ @NonNull protected final AppImportanceFilter mAppImportanceFilter;
+ @NonNull protected final Supplier<Throttler> mThrottlerSupplier;
+ @NonNull protected final FledgeAuthorizationFilter mFledgeAuthorizationFilter;
+ @NonNull protected final FledgeAllowListsFilter mFledgeAllowListsFilter;
+ @NonNull protected final ApiServiceLatencyCalculator mApiServiceLatencyCalculator;
+ protected final int mCallerUid;
+ /**
+ * @param context service context
+ * @param customAudienceDao DAO to access custom audience storage
+ * @param adSelectionEntryDao DAO to access ad selection storage
+ * @param lightweightExecutorService executor for running short tasks
+ * @param backgroundExecutorService executor for longer running tasks (ex. network calls)
+ * @param scheduledExecutor executor for tasks to be run with a delay or timed executions
+ * @param consentManager instance of {@link ConsentManager} for verifying user consent
+ * @param adServicesLogger logger for logging calls to PPAPI
+ * @param appImportanceFilter filter to assert calling app is running in the foreground
+ * @param flags for accessing feature flags
+ * @param throttlerSupplier supplier for throttling calls to PPAPI
+ * @param callerUid calling app UID
+ * @param fledgeAuthorizationFilter filter for authorizing the caller on certain behavior
+ * @param fledgeAllowListsFilter filter for verifying the caller can call PPAPI
+ */
public AdSelectionRunner(
@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 ConsentManager consentManager,
@NonNull final AdServicesLogger adServicesLogger,
- @NonNull final DevContext devContext,
@NonNull AppImportanceFilter appImportanceFilter,
@NonNull final Flags flags,
@NonNull final Supplier<Throttler> throttlerSupplier,
int callerUid,
@NonNull final FledgeAuthorizationFilter fledgeAuthorizationFilter,
- @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter) {
+ @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter,
+ @NonNull final ApiServiceLatencyCalculator apiServiceLatencyCalculator) {
Objects.requireNonNull(context);
Objects.requireNonNull(customAudienceDao);
Objects.requireNonNull(adSelectionEntryDao);
- Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(lightweightExecutorService);
Objects.requireNonNull(backgroundExecutorService);
Objects.requireNonNull(consentManager);
Objects.requireNonNull(adServicesLogger);
- Objects.requireNonNull(devContext);
Objects.requireNonNull(appImportanceFilter);
Objects.requireNonNull(flags);
Objects.requireNonNull(throttlerSupplier);
Objects.requireNonNull(fledgeAuthorizationFilter);
Objects.requireNonNull(fledgeAllowListsFilter);
+ Preconditions.checkArgument(
+ JSScriptEngine.AvailabilityChecker.isJSSandboxAvailable(),
+ JS_SANDBOX_IS_NOT_AVAILABLE);
+ Objects.requireNonNull(apiServiceLatencyCalculator);
+
mContext = context;
mCustomAudienceDao = customAudienceDao;
mAdSelectionEntryDao = adSelectionEntryDao;
- mAdServicesHttpsClient = adServicesHttpsClient;
mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService);
mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService);
+ mScheduledExecutor = scheduledExecutor;
mConsentManager = consentManager;
mAdServicesLogger = adServicesLogger;
- mAdsScoreGenerator =
- new AdsScoreGeneratorImpl(
- new AdSelectionScriptEngine(
- mContext,
- () -> flags.getEnforceIsolateMaxHeapSize(),
- () -> flags.getIsolateMaxHeapSizeBytes()),
- mLightweightExecutorService,
- mBackgroundExecutorService,
- mAdServicesHttpsClient,
- devContext,
- mAdSelectionEntryDao,
- flags);
- mAdBidGenerator =
- new AdBidGeneratorImpl(
- context,
- mAdServicesHttpsClient,
- mLightweightExecutorService,
- mBackgroundExecutorService,
- devContext,
- mCustomAudienceDao,
- flags);
mAdSelectionIdGenerator = new AdSelectionIdGenerator();
mClock = Clock.systemUTC();
mFlags = flags;
@@ -200,6 +193,7 @@ public final class AdSelectionRunner {
mCallerUid = callerUid;
mFledgeAuthorizationFilter = fledgeAuthorizationFilter;
mFledgeAllowListsFilter = fledgeAllowListsFilter;
+ mApiServiceLatencyCalculator = apiServiceLatencyCalculator;
}
@VisibleForTesting
@@ -207,12 +201,10 @@ public final class AdSelectionRunner {
@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 ConsentManager consentManager,
- @NonNull final AdsScoreGenerator adsScoreGenerator,
- @NonNull final AdBidGenerator adBidGenerator,
@NonNull final AdSelectionIdGenerator adSelectionIdGenerator,
@NonNull Clock clock,
@NonNull final AdServicesLogger adServicesLogger,
@@ -221,32 +213,30 @@ public final class AdSelectionRunner {
@NonNull final Supplier<Throttler> throttlerSupplier,
int callerUid,
@NonNull final FledgeAuthorizationFilter fledgeAuthorizationFilter,
- @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter) {
+ @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter,
+ @NonNull final ApiServiceLatencyCalculator apiServiceLatencyCalculator) {
Objects.requireNonNull(context);
Objects.requireNonNull(customAudienceDao);
Objects.requireNonNull(adSelectionEntryDao);
- Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(lightweightExecutorService);
Objects.requireNonNull(backgroundExecutorService);
+ Objects.requireNonNull(scheduledExecutor);
Objects.requireNonNull(consentManager);
- Objects.requireNonNull(adsScoreGenerator);
- Objects.requireNonNull(adBidGenerator);
Objects.requireNonNull(adSelectionIdGenerator);
Objects.requireNonNull(clock);
Objects.requireNonNull(adServicesLogger);
Objects.requireNonNull(appImportanceFilter);
Objects.requireNonNull(flags);
Objects.requireNonNull(fledgeAuthorizationFilter);
+ Objects.requireNonNull(apiServiceLatencyCalculator);
mContext = context;
mCustomAudienceDao = customAudienceDao;
mAdSelectionEntryDao = adSelectionEntryDao;
- mAdServicesHttpsClient = adServicesHttpsClient;
mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService);
mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService);
+ mScheduledExecutor = scheduledExecutor;
mConsentManager = consentManager;
- mAdsScoreGenerator = adsScoreGenerator;
- mAdBidGenerator = adBidGenerator;
mAdSelectionIdGenerator = adSelectionIdGenerator;
mClock = clock;
mAdServicesLogger = adServicesLogger;
@@ -256,6 +246,7 @@ public final class AdSelectionRunner {
mCallerUid = callerUid;
mFledgeAuthorizationFilter = fledgeAuthorizationFilter;
mFledgeAllowListsFilter = fledgeAllowListsFilter;
+ mApiServiceLatencyCalculator = apiServiceLatencyCalculator;
}
/**
@@ -293,8 +284,6 @@ public final class AdSelectionRunner {
@Override
public void onSuccess(DBAdSelection result) {
notifySuccessToCaller(result, callback);
- // TODO(242280808): Schedule a clear for stale data instead of this hack
- clearExpiredAdSelectionData();
}
@Override
@@ -306,12 +295,11 @@ public final class AdSelectionRunner {
} else {
notifyFailureToCaller(callback, t);
}
- // TODO(242280808): Schedule a clear for stale data instead of this hack
- clearExpiredAdSelectionData();
}
},
mLightweightExecutorService);
} catch (Throwable t) {
+ LogUtil.v("run ad selection fails fast with exception %s.", t.toString());
notifyFailureToCaller(callback, t);
}
}
@@ -330,11 +318,15 @@ public final class AdSelectionRunner {
LogUtil.e(e, "Encountered exception during notifying AdSelection callback");
resultCode = AdServicesStatusUtils.STATUS_UNKNOWN_ERROR;
} finally {
+ int overallLatencyMs = mApiServiceLatencyCalculator.getApiServiceOverallLatencyMs();
LogUtil.v(
- "Ad Selection with Id:%d completed, attempted notifying success",
- result.getAdSelectionId());
+ "Ad Selection with Id:%d completed with overall latency %d in ms, "
+ + "attempted notifying success",
+ result.getAdSelectionId(), overallLatencyMs);
+ // TODO(b//253522566): When including logging data from bidding & auction server side
+ // should be able to differentiate the data from the on-device telemetry.
mAdServicesLogger.logFledgeApiCallStats(
- AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS, resultCode);
+ AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS, resultCode, overallLatencyMs);
}
}
@@ -350,8 +342,15 @@ public final class AdSelectionRunner {
LogUtil.e(e, "Encountered exception during notifying AdSelection callback");
resultCode = AdServicesStatusUtils.STATUS_UNKNOWN_ERROR;
} finally {
+ int overallLatencyMs = mApiServiceLatencyCalculator.getApiServiceOverallLatencyMs();
+ LogUtil.v(
+ "Ad Selection with Id:%d completed with overall latency %d in ms, "
+ + "attempted notifying success for a silent failure",
+ mAdSelectionIdGenerator.generateId(), overallLatencyMs);
+ // TODO(b//253522566): When including logging data from bidding & auction server side
+ // should be able to differentiate the data from the on-device telemetry.
mAdServicesLogger.logFledgeApiCallStats(
- AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS, resultCode);
+ AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS, resultCode, overallLatencyMs);
}
}
@@ -372,6 +371,8 @@ public final class AdSelectionRunner {
resultCode = AdServicesStatusUtils.STATUS_INVALID_ARGUMENT;
} else if (t instanceof LimitExceededException) {
resultCode = AdServicesStatusUtils.STATUS_RATE_LIMIT_REACHED;
+ } else if (t instanceof JSSandboxIsNotAvailableException) {
+ resultCode = AdServicesStatusUtils.STATUS_JS_SANDBOX_UNAVAILABLE;
} else {
resultCode = AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
}
@@ -391,8 +392,12 @@ public final class AdSelectionRunner {
LogUtil.e(e, "Encountered exception during notifying AdSelection callback");
resultCode = AdServicesStatusUtils.STATUS_UNKNOWN_ERROR;
} finally {
+ int overallLatencyMs = mApiServiceLatencyCalculator.getApiServiceOverallLatencyMs();
+ LogUtil.v("Ad Selection failed with overall latency %d in ms", overallLatencyMs);
+ // TODO(b//253522566): When including logging data from bidding & auction server side
+ // should be able to differentiate the data from the on-device telemetry.
mAdServicesLogger.logFledgeApiCallStats(
- AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS, resultCode);
+ AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS, resultCode, overallLatencyMs);
}
}
@@ -410,60 +415,34 @@ public final class AdSelectionRunner {
ListenableFuture<List<DBCustomAudience>> buyerCustomAudience =
getBuyersCustomAudience(adSelectionConfig);
+ ListenableFuture<AdSelectionOrchestrationResult> dbAdSelection =
+ orchestrateAdSelection(adSelectionConfig, callerPackageName, buyerCustomAudience);
- AsyncFunction<List<DBCustomAudience>, List<AdBiddingOutcome>> bidAds =
- buyerCAs -> {
- return runAdBidding(buyerCAs, adSelectionConfig);
- };
-
- ListenableFuture<List<AdBiddingOutcome>> biddingOutcome =
- Futures.transformAsync(buyerCustomAudience, bidAds, mLightweightExecutorService);
-
- AsyncFunction<List<AdBiddingOutcome>, List<AdScoringOutcome>> mapBidsToScores =
- bids -> {
- return runAdScoring(bids, adSelectionConfig);
- };
-
- ListenableFuture<List<AdScoringOutcome>> scoredAds =
- Futures.transformAsync(
- biddingOutcome, mapBidsToScores, mLightweightExecutorService);
-
- Function<List<AdScoringOutcome>, AdScoringOutcome> reduceScoresToWinner =
- scores -> {
- return getWinningOutcome(scores);
- };
-
- ListenableFuture<AdScoringOutcome> winningOutcome =
- Futures.transform(scoredAds, reduceScoresToWinner, mLightweightExecutorService);
-
- Function<AdScoringOutcome, Pair<DBAdSelection.Builder, String>> mapWinnerToDBResult =
- scoringWinner -> {
- return createAdSelectionResult(scoringWinner);
- };
-
- ListenableFuture<Pair<DBAdSelection.Builder, String>> dbAdSelectionBuilder =
- Futures.transform(winningOutcome, mapWinnerToDBResult, mLightweightExecutorService);
-
- AsyncFunction<Pair<DBAdSelection.Builder, String>, DBAdSelection> saveResultToPersistence =
+ AsyncFunction<AdSelectionOrchestrationResult, DBAdSelection> saveResultToPersistence =
adSelectionAndJs -> {
return persistAdSelection(
- adSelectionAndJs.first, adSelectionAndJs.second, callerPackageName);
+ adSelectionAndJs.mDbAdSelectionBuilder,
+ adSelectionAndJs.mBuyerDecisionLogicJs,
+ callerPackageName);
};
- return FluentFuture.from(dbAdSelectionBuilder)
+ return FluentFuture.from(dbAdSelection)
.transformAsync(saveResultToPersistence, mLightweightExecutorService)
.withTimeout(
mFlags.getAdSelectionOverallTimeoutMs(),
TimeUnit.MILLISECONDS,
- // TODO(b/237103033): Comply with thread usage policy for AdServices;
- // use a global scheduled executor
- new ScheduledThreadPoolExecutor(1))
+ mScheduledExecutor)
.catching(
TimeoutException.class,
this::handleTimeoutError,
mLightweightExecutorService);
}
+ abstract ListenableFuture<AdSelectionOrchestrationResult> orchestrateAdSelection(
+ @NonNull AdSelectionConfig adSelectionConfig,
+ @NonNull String callerPackageName,
+ @NonNull ListenableFuture<List<DBCustomAudience>> buyerCustomAudience);
+
@Nullable
private DBAdSelection handleTimeoutError(TimeoutException e) {
LogUtil.e(e, "Ad Selection exceeded time limit");
@@ -491,136 +470,6 @@ public final class AdSelectionRunner {
});
}
- private ListenableFuture<List<AdBiddingOutcome>> runAdBidding(
- @NonNull final List<DBCustomAudience> customAudiences,
- @NonNull final AdSelectionConfig adSelectionConfig)
- throws InterruptedException, ExecutionException {
- if (customAudiences.isEmpty()) {
- LogUtil.w("Cannot invoke bidding on empty list of CAs");
- return Futures.immediateFailedFuture(new Throwable("No CAs found for selection"));
- }
-
- // TODO(b/237004875) : Use common thread pool for parallel execution if possible
- ForkJoinPool customThreadPool = new ForkJoinPool(getParallelBiddingCount());
- final AtomicReference<List<ListenableFuture<AdBiddingOutcome>>> bidWinningAds =
- new AtomicReference<>();
-
- try {
- LogUtil.d("Triggering bidding for all %d custom audiences", customAudiences.size());
- customThreadPool
- .submit(
- () -> {
- LogUtil.v("Invoking bidding for #%d CAs", customAudiences.size());
- bidWinningAds.set(
- customAudiences.parallelStream()
- .map(
- customAudience -> {
- return runAdBiddingPerCA(
- customAudience,
- adSelectionConfig);
- })
- .collect(Collectors.toList()));
- })
- .get();
- } catch (InterruptedException e) {
- final String exceptionReason = "Bidding Interrupted Exception";
- LogUtil.e(e, exceptionReason);
- throw new InterruptedException(exceptionReason);
- } catch (ExecutionException e) {
- final String exceptionReason = "Bidding Execution Exception";
- LogUtil.e(e, exceptionReason);
- throw new ExecutionException(e.getCause());
- } finally {
- customThreadPool.shutdownNow();
- }
- return Futures.successfulAsList(bidWinningAds.get());
- }
-
- private int getParallelBiddingCount() {
- int parallelBiddingCountConfigValue = mFlags.getAdSelectionConcurrentBiddingCount();
- int numberOfAvailableProcessors = Runtime.getRuntime().availableProcessors();
- return Math.min(parallelBiddingCountConfigValue, numberOfAvailableProcessors);
- }
-
- private ListenableFuture<AdBiddingOutcome> runAdBiddingPerCA(
- @NonNull final DBCustomAudience customAudience,
- @NonNull final AdSelectionConfig adSelectionConfig) {
- LogUtil.v(String.format("Invoking bidding for CA: %s", customAudience.getName()));
-
- // TODO(b/233239475) : Validate Buyer signals in Ad Selection Config
- AdSelectionSignals buyerSignal =
- Optional.ofNullable(
- adSelectionConfig
- .getPerBuyerSignals()
- .get(customAudience.getBuyer()))
- .orElse(AdSelectionSignals.EMPTY);
- return mAdBidGenerator.runAdBiddingPerCA(
- customAudience,
- adSelectionConfig.getAdSelectionSignals(),
- buyerSignal,
- AdSelectionSignals.EMPTY,
- adSelectionConfig);
- // TODO(b/230569187): get the contextualSignal securely = "invoking app name"
- }
-
- @SuppressLint("DefaultLocale")
- private ListenableFuture<List<AdScoringOutcome>> runAdScoring(
- @NonNull final List<AdBiddingOutcome> adBiddingOutcomes,
- @NonNull final AdSelectionConfig adSelectionConfig)
- throws AdServicesException {
- LogUtil.v("Got %d bidding outcomes", adBiddingOutcomes.size());
- List<AdBiddingOutcome> validBiddingOutcomes =
- adBiddingOutcomes.stream().filter(Objects::nonNull).collect(Collectors.toList());
-
- if (validBiddingOutcomes.isEmpty()) {
- LogUtil.w("Received empty list of Bidding outcomes");
- throw new IllegalStateException(ERROR_NO_VALID_BIDS_FOR_SCORING);
- }
- return mAdsScoreGenerator.runAdScoring(validBiddingOutcomes, adSelectionConfig);
- }
-
- private AdScoringOutcome getWinningOutcome(
- @NonNull List<AdScoringOutcome> overallAdScoringOutcome) {
- LogUtil.v("Scoring completed, generating winning outcome");
- return overallAdScoringOutcome.stream()
- .filter(a -> a.getAdWithScore().getScore() > 0)
- .max(
- (a, b) ->
- Double.compare(
- a.getAdWithScore().getScore(),
- b.getAdWithScore().getScore()))
- .orElseThrow(() -> new IllegalStateException(ERROR_NO_WINNING_AD_FOUND));
- }
-
- /**
- * 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 scoringWinner Winning Ad for overall Ad Selection
- * @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
- Pair<DBAdSelection.Builder, String> createAdSelectionResult(
- @NonNull AdScoringOutcome scoringWinner) {
- DBAdSelection.Builder dbAdSelectionBuilder = new DBAdSelection.Builder();
- LogUtil.v("Creating Ad Selection result from scoring winner");
- dbAdSelectionBuilder
- .setWinningAdBid(scoringWinner.getAdWithScore().getAdWithBid().getBid())
- .setCustomAudienceSignals(
- scoringWinner.getCustomAudienceBiddingInfo().getCustomAudienceSignals())
- .setWinningAdRenderUri(
- scoringWinner.getAdWithScore().getAdWithBid().getAdData().getRenderUri())
- .setBiddingLogicUri(
- scoringWinner.getCustomAudienceBiddingInfo().getBiddingLogicUri())
- .setContextualSignals("{}");
- // TODO(b/230569187): get the contextualSignal securely = "invoking app name"
- return Pair.create(
- dbAdSelectionBuilder,
- scoringWinner.getCustomAudienceBiddingInfo().getBuyerDecisionLogicJs());
- }
-
private ListenableFuture<DBAdSelection> persistAdSelection(
@NonNull DBAdSelection.Builder dbAdSelectionBuilder,
@NonNull String buyerDecisionLogicJS,
@@ -654,7 +503,7 @@ public final class AdSelectionRunner {
* user consent
*/
private Void assertCallerHasUserConsent() throws ConsentManager.RevokedConsentException {
- if (!mConsentManager.getConsent(mContext.getPackageManager()).isGiven()) {
+ if (!mConsentManager.getConsent().isGiven()) {
throw new ConsentManager.RevokedConsentException();
}
return null;
@@ -793,8 +642,14 @@ public final class AdSelectionRunner {
return null;
}
- private void clearExpiredAdSelectionData() {
- Instant expirationTime = mClock.instant().minusSeconds(DAY_IN_SECONDS);
- mAdSelectionEntryDao.removeExpiredAdSelection(expirationTime);
+ static class AdSelectionOrchestrationResult {
+ DBAdSelection.Builder mDbAdSelectionBuilder;
+ String mBuyerDecisionLogicJs;
+
+ AdSelectionOrchestrationResult(
+ DBAdSelection.Builder dbAdSelectionBuilder, String buyerDecisionLogicJs) {
+ this.mDbAdSelectionBuilder = dbAdSelectionBuilder;
+ this.mBuyerDecisionLogicJs = buyerDecisionLogicJs;
+ }
}
}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionScriptEngine.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionScriptEngine.java
index 9644c540c5..6e8f38f6d3 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionScriptEngine.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionScriptEngine.java
@@ -73,8 +73,10 @@ public class AdSelectionScriptEngine {
public static final String PER_BUYER_SIGNALS_ARG_NAME = "__rb_per_buyer_signals";
public static final String TRUSTED_BIDDING_SIGNALS_ARG_NAME = "__rb_trusted_bidding_signals";
public static final String CONTEXTUAL_SIGNALS_ARG_NAME = "__rb_contextual_signals";
- public static final String USER_SIGNALS_ARG_NAME = "__rb_user_signals";
- public static final String CUSTOM_AUDIENCE_SIGNALS_ARG_NAME = "__rb_custom_audience_signals";
+ public static final String CUSTOM_AUDIENCE_BIDDING_SIGNALS_ARG_NAME =
+ "__rb_custom_audience_bidding_signals";
+ public static final String CUSTOM_AUDIENCE_SCORING_SIGNALS_ARG_NAME =
+ "__rb_custom_audience_scoring_signals";
public static final String AUCTION_CONFIG_ARG_NAME = "__rb_auction_config";
public static final String SELLER_SIGNALS_ARG_NAME = "__rb_seller_signals";
public static final String TRUSTED_SCORING_SIGNALS_ARG_NAME = "__rb_trusted_scoring_signals";
@@ -147,7 +149,6 @@ public class AdSelectionScriptEngine {
@NonNull AdSelectionSignals perBuyerSignals,
@NonNull AdSelectionSignals trustedBiddingSignals,
@NonNull AdSelectionSignals contextualSignals,
- @NonNull AdSelectionSignals userSignals,
@NonNull CustomAudienceSignals customAudienceSignals)
throws JSONException {
Objects.requireNonNull(generateBidJS);
@@ -156,7 +157,6 @@ public class AdSelectionScriptEngine {
Objects.requireNonNull(perBuyerSignals);
Objects.requireNonNull(trustedBiddingSignals);
Objects.requireNonNull(contextualSignals);
- Objects.requireNonNull(userSignals);
Objects.requireNonNull(customAudienceSignals);
ImmutableList<JSScriptArgument> signals =
@@ -168,10 +168,10 @@ public class AdSelectionScriptEngine {
TRUSTED_BIDDING_SIGNALS_ARG_NAME,
trustedBiddingSignals.toString()))
.add(jsonArg(CONTEXTUAL_SIGNALS_ARG_NAME, contextualSignals.toString()))
- .add(jsonArg(USER_SIGNALS_ARG_NAME, userSignals.toString()))
.add(
CustomAudienceBiddingSignalsArgument.asScriptArgument(
- CUSTOM_AUDIENCE_SIGNALS_ARG_NAME, customAudienceSignals))
+ CUSTOM_AUDIENCE_BIDDING_SIGNALS_ARG_NAME,
+ customAudienceSignals))
.build();
ImmutableList.Builder<JSScriptArgument> adDataArguments = new ImmutableList.Builder<>();
@@ -221,7 +221,7 @@ public class AdSelectionScriptEngine {
.add(jsonArg(CONTEXTUAL_SIGNALS_ARG_NAME, contextualSignals.toString()))
.add(
CustomAudienceScoringSignalsArgument.asScriptArgument(
- CUSTOM_AUDIENCE_SIGNALS_ARG_NAME,
+ CUSTOM_AUDIENCE_SCORING_SIGNALS_ARG_NAME,
customAudienceSignalsList))
.build();
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java
index 85070de404..dadd927f8e 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java
@@ -30,6 +30,7 @@ import android.adservices.adselection.ReportImpressionCallback;
import android.adservices.adselection.ReportImpressionInput;
import android.adservices.common.AdSelectionSignals;
import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.CallerMetadata;
import android.annotation.NonNull;
import android.content.Context;
@@ -56,10 +57,13 @@ import com.android.adservices.service.js.JSScriptEngine;
import com.android.adservices.service.stats.AdServicesLogger;
import com.android.adservices.service.stats.AdServicesLoggerImpl;
import com.android.adservices.service.stats.AdServicesStatsLog;
+import com.android.adservices.service.stats.ApiServiceLatencyCalculator;
+import com.android.adservices.service.stats.Clock;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
/**
* Implementation of {@link AdSelectionService}.
@@ -73,6 +77,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
@NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
@NonNull private final ExecutorService mLightweightExecutor;
@NonNull private final ExecutorService mBackgroundExecutor;
+ @NonNull private final ScheduledThreadPoolExecutor mScheduledExecutor;
@NonNull private final Context mContext;
@NonNull private final ConsentManager mConsentManager;
@NonNull private final DevContextFilter mDevContextFilter;
@@ -96,6 +101,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
@NonNull AppImportanceFilter appImportanceFilter,
@NonNull ExecutorService lightweightExecutorService,
@NonNull ExecutorService backgroundExecutorService,
+ @NonNull ScheduledThreadPoolExecutor scheduledExecutor,
@NonNull Context context,
ConsentManager consentManager,
@NonNull AdServicesLogger adServicesLogger,
@@ -111,6 +117,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
Objects.requireNonNull(appImportanceFilter);
Objects.requireNonNull(lightweightExecutorService);
Objects.requireNonNull(backgroundExecutorService);
+ Objects.requireNonNull(scheduledExecutor);
Objects.requireNonNull(consentManager);
Objects.requireNonNull(adServicesLogger);
Objects.requireNonNull(flags);
@@ -124,6 +131,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
mAppImportanceFilter = appImportanceFilter;
mLightweightExecutor = lightweightExecutorService;
mBackgroundExecutor = backgroundExecutorService;
+ mScheduledExecutor = scheduledExecutor;
mContext = context;
mConsentManager = consentManager;
mAdServicesLogger = adServicesLogger;
@@ -151,6 +159,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
() -> FlagsFactory.getFlags().getForegroundStatuslLevelForValidation()),
AdServicesExecutors.getLightWeightExecutor(),
AdServicesExecutors.getBackgroundExecutor(),
+ AdServicesExecutors.getScheduler(),
context,
ConsentManager.getInstance(context),
AdServicesLoggerImpl.getInstance(),
@@ -164,43 +173,107 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
// TODO(b/233116758): Validate all the fields inside the adSelectionConfig.
@Override
public void runAdSelection(
- @NonNull AdSelectionInput inputParams, @NonNull AdSelectionCallback callback) {
+ @NonNull AdSelectionInput inputParams,
+ @NonNull CallerMetadata callerMetadata,
+ @NonNull AdSelectionCallback callback) {
+ final ApiServiceLatencyCalculator apiServiceLatencyCalculator =
+ new ApiServiceLatencyCalculator(callerMetadata, Clock.SYSTEM_CLOCK);
int apiName = AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS;
// Caller permissions must be checked in the binder thread, before anything else
mFledgeAuthorizationFilter.assertAppDeclaredPermission(mContext, apiName);
-
try {
Objects.requireNonNull(inputParams);
Objects.requireNonNull(callback);
} catch (NullPointerException exception) {
+ int overallLatencyMs = apiServiceLatencyCalculator.getApiServiceOverallLatencyMs();
+ LogUtil.v(
+ "The runAdSelection() arguments should not be null, failed with overall"
+ + "latency %d in ms.",
+ overallLatencyMs);
mAdServicesLogger.logFledgeApiCallStats(
- apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT);
+ apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT, overallLatencyMs);
// Rethrow because we want to fail fast
throw exception;
}
DevContext devContext = mDevContextFilter.createDevContext();
+ int callerUid = getCallingUid(apiName);
+ mLightweightExecutor.execute(
+ () -> {
+ // TODO(b/249298855): Evolve off device ad selection logic.
+ if (mFlags.getAdSelectionOffDeviceEnabled()) {
+ runOffDeviceAdSelection(
+ devContext,
+ callerUid,
+ inputParams,
+ callback,
+ apiServiceLatencyCalculator);
+ } else {
+ runOnDeviceAdSelection(
+ devContext,
+ callerUid,
+ inputParams,
+ callback,
+ apiServiceLatencyCalculator);
+ }
+ });
+ }
- AdSelectionRunner adSelectionRunner =
- new AdSelectionRunner(
+ private void runOnDeviceAdSelection(
+ DevContext devContext,
+ int callerUid,
+ @NonNull AdSelectionInput inputParams,
+ @NonNull AdSelectionCallback callback,
+ @NonNull ApiServiceLatencyCalculator apiServiceLatencyCalculator) {
+ OnDeviceAdSelectionRunner runner =
+ new OnDeviceAdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mAdServicesHttpsClient,
mLightweightExecutor,
mBackgroundExecutor,
+ mScheduledExecutor,
mConsentManager,
mAdServicesLogger,
devContext,
mAppImportanceFilter,
mFlags,
() -> Throttler.getInstance(mFlags.getSdkRequestPermitsPerSecond()),
- getCallingUid(apiName),
+ callerUid,
mFledgeAuthorizationFilter,
- mFledgeAllowListsFilter);
+ mFledgeAllowListsFilter,
+ apiServiceLatencyCalculator);
+ runner.runAdSelection(inputParams, callback);
+ }
- adSelectionRunner.runAdSelection(inputParams, callback);
+ private void runOffDeviceAdSelection(
+ DevContext devContext,
+ int callerUid,
+ @NonNull AdSelectionInput inputParams,
+ @NonNull AdSelectionCallback callback,
+ @NonNull ApiServiceLatencyCalculator apiServiceLatencyCalculator) {
+ TrustedServerAdSelectionRunner runner =
+ new TrustedServerAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutor,
+ mBackgroundExecutor,
+ mScheduledExecutor,
+ mConsentManager,
+ mAdServicesLogger,
+ devContext,
+ mAppImportanceFilter,
+ mFlags,
+ () -> Throttler.getInstance(mFlags.getSdkRequestPermitsPerSecond()),
+ callerUid,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ apiServiceLatencyCalculator);
+ runner.runAdSelection(inputParams, callback);
}
@Override
@@ -217,7 +290,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
Objects.requireNonNull(callback);
} catch (NullPointerException exception) {
mAdServicesLogger.logFledgeApiCallStats(
- apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT);
+ apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT, 0);
// Rethrow because we want to fail fast
throw exception;
}
@@ -229,6 +302,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
mContext,
mLightweightExecutor,
mBackgroundExecutor,
+ mScheduledExecutor,
mAdSelectionEntryDao,
mAdServicesHttpsClient,
mConsentManager,
@@ -260,7 +334,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
Objects.requireNonNull(callback);
} catch (NullPointerException exception) {
mAdServicesLogger.logFledgeApiCallStats(
- apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT);
+ apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT, 0);
// Rethrow because we want to fail fast
throw exception;
}
@@ -269,7 +343,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
if (!devContext.getDevOptionsEnabled()) {
mAdServicesLogger.logFledgeApiCallStats(
- apiName, AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
+ apiName, AdServicesStatusUtils.STATUS_INTERNAL_ERROR, 0);
throw new SecurityException(API_NOT_AUTHORIZED_MSG);
}
@@ -294,7 +368,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
return mCallingAppUidSupplier.getCallingAppUid();
} catch (IllegalStateException illegalStateException) {
mAdServicesLogger.logFledgeApiCallStats(
- apiNameLoggingId, AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
+ apiNameLoggingId, AdServicesStatusUtils.STATUS_INTERNAL_ERROR, 0);
throw illegalStateException;
}
}
@@ -315,7 +389,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
Objects.requireNonNull(callback);
} catch (NullPointerException exception) {
mAdServicesLogger.logFledgeApiCallStats(
- apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT);
+ apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT, 0);
// Rethrow because we want to fail fast
throw exception;
}
@@ -324,7 +398,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
if (!devContext.getDevOptionsEnabled()) {
mAdServicesLogger.logFledgeApiCallStats(
- apiName, AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
+ apiName, AdServicesStatusUtils.STATUS_INTERNAL_ERROR, 0);
throw new SecurityException(API_NOT_AUTHORIZED_MSG);
}
@@ -358,7 +432,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
Objects.requireNonNull(callback);
} catch (NullPointerException exception) {
mAdServicesLogger.logFledgeApiCallStats(
- apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT);
+ apiName, AdServicesStatusUtils.STATUS_INVALID_ARGUMENT, 0);
// Rethrow because we want to fail fast
throw exception;
}
@@ -367,7 +441,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub {
if (!devContext.getDevOptionsEnabled()) {
mAdServicesLogger.logFledgeApiCallStats(
- apiName, AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
+ apiName, AdServicesStatusUtils.STATUS_INTERNAL_ERROR, 0);
throw new SecurityException(API_NOT_AUTHORIZED_MSG);
}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdsScoreGeneratorImpl.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdsScoreGeneratorImpl.java
index 7a8101f2b4..33fdf528e1 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/AdsScoreGeneratorImpl.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdsScoreGeneratorImpl.java
@@ -69,6 +69,7 @@ public class AdsScoreGeneratorImpl implements AdsScoreGenerator {
@NonNull private final AdSelectionScriptEngine mAdSelectionScriptEngine;
@NonNull private final ListeningExecutorService mLightweightExecutorService;
@NonNull private final ListeningExecutorService mBackgroundExecutorService;
+ @NonNull private final ScheduledThreadPoolExecutor mScheduledExecutor;
@NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
@NonNull private final AdSelectionDevOverridesHelper mAdSelectionDevOverridesHelper;
@NonNull private final Flags mFlags;
@@ -77,6 +78,7 @@ public class AdsScoreGeneratorImpl implements AdsScoreGenerator {
@NonNull AdSelectionScriptEngine adSelectionScriptEngine,
@NonNull ListeningExecutorService lightweightExecutor,
@NonNull ListeningExecutorService backgroundExecutor,
+ @NonNull ScheduledThreadPoolExecutor scheduledExecutor,
@NonNull AdServicesHttpsClient adServicesHttpsClient,
@NonNull DevContext devContext,
@NonNull AdSelectionEntryDao adSelectionEntryDao,
@@ -84,6 +86,7 @@ public class AdsScoreGeneratorImpl implements AdsScoreGenerator {
Objects.requireNonNull(adSelectionScriptEngine);
Objects.requireNonNull(lightweightExecutor);
Objects.requireNonNull(backgroundExecutor);
+ Objects.requireNonNull(scheduledExecutor);
Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(devContext);
Objects.requireNonNull(adSelectionEntryDao);
@@ -93,6 +96,7 @@ public class AdsScoreGeneratorImpl implements AdsScoreGenerator {
mAdServicesHttpsClient = adServicesHttpsClient;
mLightweightExecutorService = lightweightExecutor;
mBackgroundExecutorService = backgroundExecutor;
+ mScheduledExecutor = scheduledExecutor;
mAdSelectionDevOverridesHelper =
new AdSelectionDevOverridesHelper(devContext, adSelectionEntryDao);
mFlags = flags;
@@ -133,9 +137,7 @@ public class AdsScoreGeneratorImpl implements AdsScoreGenerator {
.withTimeout(
mFlags.getAdSelectionScoringTimeoutMs(),
TimeUnit.MILLISECONDS,
- // TODO(b/237103033): Comply with thread usage policy for AdServices;
- // use a global scheduled executor
- new ScheduledThreadPoolExecutor(1))
+ mScheduledExecutor)
.catching(
TimeoutException.class,
this::handleTimeoutError,
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/CustomAudienceReportingSignalsArgument.java b/adservices/service-core/java/com/android/adservices/service/adselection/CustomAudienceReportingSignalsArgument.java
new file mode 100644
index 0000000000..e2a97527e9
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/CustomAudienceReportingSignalsArgument.java
@@ -0,0 +1,49 @@
+/*
+ * 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 com.android.adservices.service.js.JSScriptArgument.recordArg;
+import static com.android.adservices.service.js.JSScriptArgument.stringArg;
+
+import com.android.adservices.data.adselection.CustomAudienceSignals;
+import com.android.adservices.service.js.JSScriptArgument;
+
+import org.json.JSONException;
+
+/**
+ * A utility class to convert instances of {@link CustomAudienceSignals} into {@link
+ * JSScriptArgument}. It strips out extraneous information from {@link CustomAudienceSignals} and
+ * only passes the data relevant for reporting.
+ */
+public class CustomAudienceReportingSignalsArgument {
+
+ // TODO: (b/228094391): Put these common constants in a separate class
+ public static final String NAME_FIELD_NAME = "name";
+
+ // No instance of this class is supposed to be created
+ private CustomAudienceReportingSignalsArgument() {}
+
+ /**
+ * @return A {@link JSScriptArgument} with the given {@code name} to represent this instance of
+ * {@link CustomAudienceReportingSignalsArgument}
+ * @throws JSONException if any of the signals in this class is not valid JSON.
+ */
+ public static JSScriptArgument asScriptArgument(
+ String name, CustomAudienceSignals customAudienceSignals) throws JSONException {
+ return recordArg(name, stringArg(NAME_FIELD_NAME, customAudienceSignals.getName()));
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/ImpressionReporter.java b/adservices/service-core/java/com/android/adservices/service/adselection/ImpressionReporter.java
index 2e6bc6fcc3..25801096c2 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/ImpressionReporter.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/ImpressionReporter.java
@@ -39,11 +39,13 @@ import com.android.adservices.data.adselection.CustomAudienceSignals;
import com.android.adservices.data.adselection.DBAdSelectionEntry;
import com.android.adservices.service.Flags;
import com.android.adservices.service.common.AdServicesHttpsClient;
+import com.android.adservices.service.common.AdTechUriValidator;
import com.android.adservices.service.common.AppImportanceFilter;
import com.android.adservices.service.common.AppImportanceFilter.WrongCallingApplicationStateException;
import com.android.adservices.service.common.FledgeAllowListsFilter;
import com.android.adservices.service.common.FledgeAuthorizationFilter;
import com.android.adservices.service.common.Throttler;
+import com.android.adservices.service.common.ValidatorUtil;
import com.android.adservices.service.consent.ConsentManager;
import com.android.adservices.service.devapi.AdSelectionDevOverridesHelper;
import com.android.adservices.service.devapi.DevContext;
@@ -74,6 +76,8 @@ public class ImpressionReporter {
public static final String CALLER_PACKAGE_NAME_MISMATCH =
"Caller package name does not match name used in ad selection";
+ private static final String REPORTING_URI_FIELD_NAME = "reporting URI";
+
@VisibleForTesting
static final String REPORT_IMPRESSION_THROTTLED = "Report impression exceeded rate limit";
@@ -82,6 +86,7 @@ public class ImpressionReporter {
@NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
@NonNull private final ListeningExecutorService mLightweightExecutorService;
@NonNull private final ListeningExecutorService mBackgroundExecutorService;
+ @NonNull private final ScheduledThreadPoolExecutor mScheduledExecutor;
@NonNull private final ReportImpressionScriptEngine mJsEngine;
@NonNull private final ConsentManager mConsentManager;
@NonNull private final AdSelectionDevOverridesHelper mAdSelectionDevOverridesHelper;
@@ -97,6 +102,7 @@ public class ImpressionReporter {
@NonNull Context context,
@NonNull ExecutorService lightweightExecutor,
@NonNull ExecutorService backgroundExecutor,
+ @NonNull ScheduledThreadPoolExecutor scheduledExecutor,
@NonNull AdSelectionEntryDao adSelectionEntryDao,
@NonNull AdServicesHttpsClient adServicesHttpsClient,
@NonNull ConsentManager consentManager,
@@ -111,6 +117,7 @@ public class ImpressionReporter {
Objects.requireNonNull(context);
Objects.requireNonNull(lightweightExecutor);
Objects.requireNonNull(backgroundExecutor);
+ Objects.requireNonNull(scheduledExecutor);
Objects.requireNonNull(adSelectionEntryDao);
Objects.requireNonNull(adServicesHttpsClient);
Objects.requireNonNull(consentManager);
@@ -125,6 +132,7 @@ public class ImpressionReporter {
mContext = context;
mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutor);
mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutor);
+ mScheduledExecutor = scheduledExecutor;
mAdSelectionEntryDao = adSelectionEntryDao;
mAdServicesHttpsClient = adServicesHttpsClient;
mJsEngine =
@@ -161,7 +169,7 @@ public class ImpressionReporter {
throw e.rethrowFromSystemServer();
} finally {
mAdServicesLogger.logFledgeApiCallStats(
- AD_SERVICES_API_CALLED__API_NAME__REPORT_IMPRESSION, resultCode);
+ AD_SERVICES_API_CALLED__API_NAME__REPORT_IMPRESSION, resultCode, 0);
}
}
@@ -177,7 +185,7 @@ public class ImpressionReporter {
// TODO(b/233681870): Investigate implementation of actual failures in
// logs/metrics
mAdServicesLogger.logFledgeApiCallStats(
- AD_SERVICES_API_CALLED__API_NAME__REPORT_IMPRESSION, resultCode);
+ AD_SERVICES_API_CALLED__API_NAME__REPORT_IMPRESSION, resultCode, 0);
}
}
@@ -239,15 +247,24 @@ public class ImpressionReporter {
requestParams.getCallerPackageName()),
mLightweightExecutorService)
.transform(
- reportingUris -> notifySuccessToCaller(callback, reportingUris),
+ reportingUrisAndContext ->
+ notifySuccessToCaller(
+ callback,
+ reportingUrisAndContext.first,
+ reportingUrisAndContext.second),
mLightweightExecutorService)
.withTimeout(
mFlags.getReportImpressionOverallTimeoutMs(),
TimeUnit.MILLISECONDS,
// TODO(b/237103033): Comply with thread usage policy for AdServices;
// use a global scheduled executor
- new ScheduledThreadPoolExecutor(1))
- .transformAsync(this::doReport, mLightweightExecutorService)
+ mScheduledExecutor)
+ .transformAsync(
+ reportingUrisAndContext ->
+ doReport(
+ reportingUrisAndContext.first,
+ reportingUrisAndContext.second),
+ mLightweightExecutorService)
.addCallback(
new FutureCallback<List<Void>>() {
@Override
@@ -270,10 +287,12 @@ public class ImpressionReporter {
mLightweightExecutorService);
}
- private ReportingUris notifySuccessToCaller(
- @NonNull ReportImpressionCallback callback, @NonNull ReportingUris reportingUris) {
+ private Pair<ReportingUris, ReportingContext> notifySuccessToCaller(
+ @NonNull ReportImpressionCallback callback,
+ @NonNull ReportingUris reportingUris,
+ @NonNull ReportingContext ctx) {
invokeSuccess(callback, AdServicesStatusUtils.STATUS_SUCCESS);
- return reportingUris;
+ return Pair.create(reportingUris, ctx);
}
private void notifyFailureToCaller(
@@ -297,22 +316,58 @@ public class ImpressionReporter {
}
@NonNull
- private ListenableFuture<List<Void>> doReport(ReportingUris reportingUris) {
+ private ListenableFuture<List<Void>> doReport(
+ ReportingUris reportingUris, ReportingContext ctx) {
LogUtil.v("Reporting URIs");
- ListenableFuture<Void> sellerFuture =
- mAdServicesHttpsClient.reportUri(reportingUris.sellerReportingUri);
+
+ ListenableFuture<Void> sellerFuture;
+
+ // Validate seller uri before reporting
+ AdTechUriValidator sellerValidator =
+ new AdTechUriValidator(
+ ValidatorUtil.AD_TECH_ROLE_SELLER,
+ ctx.mAdSelectionConfig.getSeller().toString(),
+ this.getClass().getSimpleName(),
+ REPORTING_URI_FIELD_NAME);
+ try {
+ sellerValidator.validate(reportingUris.sellerReportingUri);
+ // Perform reporting if no exception was thrown
+ sellerFuture = mAdServicesHttpsClient.reportUri(reportingUris.sellerReportingUri);
+ } catch (IllegalArgumentException e) {
+ LogUtil.v("Seller reporting URI validation failed!");
+ sellerFuture = Futures.immediateFuture(null);
+ }
+
ListenableFuture<Void> buyerFuture;
+ // Validate buyer uri if it exists
if (!Objects.isNull(reportingUris.buyerReportingUri)) {
- buyerFuture = mAdServicesHttpsClient.reportUri(reportingUris.buyerReportingUri);
+ CustomAudienceSignals customAudienceSignals =
+ Objects.requireNonNull(ctx.mDBAdSelectionEntry.getCustomAudienceSignals());
+
+ AdTechUriValidator buyerValidator =
+ new AdTechUriValidator(
+ ValidatorUtil.AD_TECH_ROLE_BUYER,
+ customAudienceSignals.getBuyer().toString(),
+ this.getClass().getSimpleName(),
+ REPORTING_URI_FIELD_NAME);
+ try {
+ buyerValidator.validate(reportingUris.buyerReportingUri);
+ // Perform reporting if no exception was thrown
+ buyerFuture = mAdServicesHttpsClient.reportUri(reportingUris.buyerReportingUri);
+ } catch (IllegalArgumentException e) {
+ LogUtil.v("Buyer reporting URI validation failed!");
+ buyerFuture = Futures.immediateFuture(null);
+ }
} else {
+ // In case of contextual ad
buyerFuture = Futures.immediateFuture(null);
}
return Futures.allAsList(sellerFuture, buyerFuture);
}
- private FluentFuture<ReportingUris> computeReportingUris(
+ private FluentFuture<Pair<ReportingUris, ReportingContext>> computeReportingUris(
long adSelectionId, AdSelectionConfig adSelectionConfig, String callerPackageName) {
return fetchAdSelectionEntry(adSelectionId, callerPackageName)
.transformAsync(
@@ -332,8 +387,7 @@ public class ImpressionReporter {
sellerResultAndCtx ->
invokeBuyerScript(
sellerResultAndCtx.first, sellerResultAndCtx.second),
- mLightweightExecutorService)
- .transform(urisAndContext -> urisAndContext.first, mLightweightExecutorService);
+ mLightweightExecutorService);
}
private FluentFuture<DBAdSelectionEntry> fetchAdSelectionEntry(
@@ -457,7 +511,7 @@ public class ImpressionReporter {
* user consent
*/
private Void assertCallerHasUserConsent() throws ConsentManager.RevokedConsentException {
- if (!mConsentManager.getConsent(mContext.getPackageManager()).isGiven()) {
+ if (!mConsentManager.getConsent().isGiven()) {
throw new ConsentManager.RevokedConsentException();
}
return null;
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/JsFetcher.java b/adservices/service-core/java/com/android/adservices/service/adselection/JsFetcher.java
new file mode 100644
index 0000000000..4efd7f778c
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/JsFetcher.java
@@ -0,0 +1,96 @@
+/*
+ * 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.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.net.Uri;
+
+import com.android.adservices.LogUtil;
+import com.android.adservices.service.common.AdServicesHttpsClient;
+import com.android.adservices.service.devapi.CustomAudienceDevOverridesHelper;
+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;
+
+/** Class to fetch JavaScript code both on and off device. */
+public class JsFetcher {
+ @VisibleForTesting
+ static final String MISSING_BIDDING_LOGIC = "Error fetching bidding js logic";
+
+ private final ListeningExecutorService mBackgroundExecutorService;
+ private final ListeningExecutorService mLightweightExecutorService;
+ private final CustomAudienceDevOverridesHelper mCustomAudienceDevOverridesHelper;
+ private final AdServicesHttpsClient mAdServicesHttpsClient;
+
+ public JsFetcher(
+ @NonNull ListeningExecutorService backgroundExecutorService,
+ @NonNull ListeningExecutorService lightweightExecutorService,
+ @NonNull CustomAudienceDevOverridesHelper customAudienceDevOverridesHelper,
+ @NonNull AdServicesHttpsClient adServicesHttpsClient) {
+ mBackgroundExecutorService = backgroundExecutorService;
+ mCustomAudienceDevOverridesHelper = customAudienceDevOverridesHelper;
+ mAdServicesHttpsClient = adServicesHttpsClient;
+ mLightweightExecutorService = lightweightExecutorService;
+ }
+
+ /**
+ * Fetch the buyer decision logic. Check locally to see if an override is present, otherwise
+ * fetch from server.
+ *
+ * @return buyer decision logic
+ */
+ public 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);
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java b/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java
new file mode 100644
index 0000000000..ba4fd5c57e
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java
@@ -0,0 +1,331 @@
+/*
+ * 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.common.AdTechIdentifier;
+import android.adservices.exceptions.AdServicesException;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.Pair;
+
+import com.android.adservices.LogUtil;
+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.AdServicesHttpsClient;
+import com.android.adservices.service.common.AppImportanceFilter;
+import com.android.adservices.service.common.FledgeAllowListsFilter;
+import com.android.adservices.service.common.FledgeAuthorizationFilter;
+import com.android.adservices.service.common.Throttler;
+import com.android.adservices.service.consent.ConsentManager;
+import com.android.adservices.service.devapi.DevContext;
+import com.android.adservices.service.stats.AdServicesLogger;
+import com.android.adservices.service.stats.ApiServiceLatencyCalculator;
+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.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.time.Clock;
+import java.util.ArrayList;
+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.function.Supplier;
+import java.util.stream.Collectors;
+
+/** Orchestrate on-device ad selection. */
+public class OnDeviceAdSelectionRunner extends AdSelectionRunner {
+ @NonNull protected final AdsScoreGenerator mAdsScoreGenerator;
+ @NonNull protected final AdServicesHttpsClient mAdServicesHttpsClient;
+ @NonNull protected final AdBidGenerator mAdBidGenerator;
+
+ 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 ConsentManager consentManager,
+ @NonNull final AdServicesLogger adServicesLogger,
+ @NonNull final DevContext devContext,
+ @NonNull AppImportanceFilter appImportanceFilter,
+ @NonNull final Flags flags,
+ @NonNull final Supplier<Throttler> throttlerSupplier,
+ int callerUid,
+ @NonNull final FledgeAuthorizationFilter fledgeAuthorizationFilter,
+ @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter,
+ @NonNull final ApiServiceLatencyCalculator apiServiceLatencyCalculator) {
+ super(
+ context,
+ customAudienceDao,
+ adSelectionEntryDao,
+ lightweightExecutorService,
+ backgroundExecutorService,
+ scheduledExecutor,
+ consentManager,
+ adServicesLogger,
+ appImportanceFilter,
+ flags,
+ throttlerSupplier,
+ callerUid,
+ fledgeAuthorizationFilter,
+ fledgeAllowListsFilter,
+ apiServiceLatencyCalculator);
+
+ Objects.requireNonNull(adServicesHttpsClient);
+
+ mAdServicesHttpsClient = adServicesHttpsClient;
+ mAdBidGenerator =
+ new AdBidGeneratorImpl(
+ context,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ devContext,
+ mCustomAudienceDao,
+ flags);
+ mAdsScoreGenerator =
+ new AdsScoreGeneratorImpl(
+ new AdSelectionScriptEngine(
+ mContext,
+ () -> flags.getEnforceIsolateMaxHeapSize(),
+ () -> flags.getIsolateMaxHeapSizeBytes()),
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mAdServicesHttpsClient,
+ devContext,
+ mAdSelectionEntryDao,
+ 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 ConsentManager consentManager,
+ @NonNull final AdsScoreGenerator adsScoreGenerator,
+ @NonNull final AdBidGenerator adBidGenerator,
+ @NonNull final AdSelectionIdGenerator adSelectionIdGenerator,
+ @NonNull Clock clock,
+ @NonNull final AdServicesLogger adServicesLogger,
+ @NonNull AppImportanceFilter appImportanceFilter,
+ @NonNull final Flags flags,
+ @NonNull final Supplier<Throttler> throttlerSupplier,
+ int callerUid,
+ @NonNull final FledgeAuthorizationFilter fledgeAuthorizationFilter,
+ @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter,
+ @NonNull final ApiServiceLatencyCalculator apiServiceLatencyCalculator) {
+ super(
+ context,
+ customAudienceDao,
+ adSelectionEntryDao,
+ lightweightExecutorService,
+ backgroundExecutorService,
+ scheduledExecutor,
+ consentManager,
+ adSelectionIdGenerator,
+ clock,
+ adServicesLogger,
+ appImportanceFilter,
+ flags,
+ throttlerSupplier,
+ callerUid,
+ fledgeAuthorizationFilter,
+ fledgeAllowListsFilter,
+ apiServiceLatencyCalculator);
+
+ Objects.requireNonNull(adsScoreGenerator);
+ Objects.requireNonNull(adServicesHttpsClient);
+ Objects.requireNonNull(adBidGenerator);
+
+ mAdsScoreGenerator = adsScoreGenerator;
+ mAdServicesHttpsClient = adServicesHttpsClient;
+ mAdBidGenerator = adBidGenerator;
+ }
+
+ /**
+ * 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) {
+ AsyncFunction<List<DBCustomAudience>, List<AdBiddingOutcome>> bidAds =
+ buyerCAs -> {
+ return runAdBidding(buyerCAs, adSelectionConfig);
+ };
+
+ ListenableFuture<List<AdBiddingOutcome>> biddingOutcome =
+ Futures.transformAsync(buyerCustomAudience, bidAds, mLightweightExecutorService);
+
+ AsyncFunction<List<AdBiddingOutcome>, List<AdScoringOutcome>> mapBidsToScores =
+ bids -> {
+ return runAdScoring(bids, adSelectionConfig);
+ };
+
+ ListenableFuture<List<AdScoringOutcome>> scoredAds =
+ Futures.transformAsync(
+ biddingOutcome, mapBidsToScores, mLightweightExecutorService);
+
+ Function<List<AdScoringOutcome>, AdScoringOutcome> reduceScoresToWinner =
+ scores -> {
+ return getWinningOutcome(scores);
+ };
+
+ ListenableFuture<AdScoringOutcome> winningOutcome =
+ Futures.transform(scoredAds, reduceScoresToWinner, mLightweightExecutorService);
+
+ Function<AdScoringOutcome, AdSelectionOrchestrationResult> mapWinnerToDBResult =
+ scoringWinner -> {
+ return createAdSelectionResult(scoringWinner);
+ };
+
+ ListenableFuture<AdSelectionOrchestrationResult> dbAdSelectionBuilder =
+ Futures.transform(winningOutcome, mapWinnerToDBResult, mLightweightExecutorService);
+
+ return dbAdSelectionBuilder;
+ }
+
+ private ListenableFuture<List<AdBiddingOutcome>> runAdBidding(
+ @NonNull final List<DBCustomAudience> customAudiences,
+ @NonNull final AdSelectionConfig adSelectionConfig) {
+ if (customAudiences.isEmpty()) {
+ LogUtil.w("Cannot invoke bidding on empty list of CAs");
+ return Futures.immediateFailedFuture(new Throwable("No CAs found for selection"));
+ }
+
+ Map<AdTechIdentifier, List<DBCustomAudience>> buyerToCustomAudienceMap =
+ mapBuyerToCustomAudience(customAudiences);
+ PerBuyerBiddingRunner buyerBidRunner =
+ new PerBuyerBiddingRunner(
+ mAdBidGenerator, mScheduledExecutor, mBackgroundExecutorService);
+
+ LogUtil.v("Invoking bidding for #%d buyers", buyerToCustomAudienceMap.size());
+ return Futures.successfulAsList(
+ buyerToCustomAudienceMap.entrySet().parallelStream()
+ .map(
+ entry -> {
+ return buyerBidRunner.runBidding(
+ entry.getKey(),
+ entry.getValue(),
+ mFlags.getAdSelectionBiddingTimeoutPerBuyerMs(),
+ adSelectionConfig);
+ })
+ .flatMap(List::stream)
+ .collect(Collectors.toList()));
+ }
+
+ @SuppressLint("DefaultLocale")
+ private ListenableFuture<List<AdScoringOutcome>> runAdScoring(
+ @NonNull final List<AdBiddingOutcome> adBiddingOutcomes,
+ @NonNull final AdSelectionConfig adSelectionConfig)
+ throws AdServicesException {
+ LogUtil.v("Got %d total bidding outcomes", adBiddingOutcomes.size());
+ List<AdBiddingOutcome> validBiddingOutcomes =
+ adBiddingOutcomes.stream().filter(Objects::nonNull).collect(Collectors.toList());
+ LogUtil.v("Got %d valid bidding outcomes", validBiddingOutcomes.size());
+
+ if (validBiddingOutcomes.isEmpty()) {
+ LogUtil.w("Received empty list of successful Bidding outcomes");
+ throw new IllegalStateException(ERROR_NO_VALID_BIDS_FOR_SCORING);
+ }
+ return mAdsScoreGenerator.runAdScoring(validBiddingOutcomes, adSelectionConfig);
+ }
+
+ private AdScoringOutcome getWinningOutcome(
+ @NonNull List<AdScoringOutcome> overallAdScoringOutcome) {
+ LogUtil.v("Scoring completed, generating winning outcome");
+ return overallAdScoringOutcome.stream()
+ .filter(a -> a.getAdWithScore().getScore() > 0)
+ .max(
+ (a, b) ->
+ Double.compare(
+ a.getAdWithScore().getScore(),
+ b.getAdWithScore().getScore()))
+ .orElseThrow(() -> new IllegalStateException(ERROR_NO_WINNING_AD_FOUND));
+ }
+
+ /**
+ * 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 scoringWinner Winning Ad for overall Ad Selection
+ * @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
+ AdSelectionOrchestrationResult createAdSelectionResult(
+ @NonNull AdScoringOutcome scoringWinner) {
+ DBAdSelection.Builder dbAdSelectionBuilder = new DBAdSelection.Builder();
+ LogUtil.v("Creating Ad Selection result from scoring winner");
+ dbAdSelectionBuilder
+ .setWinningAdBid(scoringWinner.getAdWithScore().getAdWithBid().getBid())
+ .setCustomAudienceSignals(
+ scoringWinner.getCustomAudienceBiddingInfo().getCustomAudienceSignals())
+ .setWinningAdRenderUri(
+ scoringWinner.getAdWithScore().getAdWithBid().getAdData().getRenderUri())
+ .setBiddingLogicUri(
+ scoringWinner.getCustomAudienceBiddingInfo().getBiddingLogicUri())
+ .setContextualSignals("{}");
+ // TODO(b/230569187): get the contextualSignal securely = "invoking app name"
+ return new AdSelectionOrchestrationResult(
+ dbAdSelectionBuilder,
+ scoringWinner.getCustomAudienceBiddingInfo().getBuyerDecisionLogicJs());
+ }
+
+ 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);
+ }
+ LogUtil.v("Created mapping for #%d buyers", buyerToCustomAudienceMap.size());
+ return buyerToCustomAudienceMap;
+ }
+
+ private int getParallelBiddingCount() {
+ int parallelBiddingCountConfigValue = mFlags.getAdSelectionConcurrentBiddingCount();
+ int numberOfAvailableProcessors = Runtime.getRuntime().availableProcessors();
+ return Math.min(parallelBiddingCountConfigValue, numberOfAvailableProcessors);
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/PerBuyerBiddingRunner.java b/adservices/service-core/java/com/android/adservices/service/adselection/PerBuyerBiddingRunner.java
new file mode 100644
index 0000000000..606a648173
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/PerBuyerBiddingRunner.java
@@ -0,0 +1,139 @@
+/*
+ * 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.common.AdSelectionSignals;
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+
+import com.android.adservices.LogUtil;
+import com.android.adservices.data.customaudience.DBCustomAudience;
+
+import com.google.common.util.concurrent.ExecutionSequencer;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * Runs bidding for a buyer and its associated Custom Audience. The bidding for every buyer is time
+ * capped, where the incomplete CAs are dropped from bidding when timed out while preserving the
+ * ones that were already completed
+ */
+public class PerBuyerBiddingRunner {
+ @NonNull private AdBidGenerator mAdBidGenerator;
+ @NonNull private ScheduledThreadPoolExecutor mScheduledExecutor;
+ @NonNull private ListeningExecutorService mBackgroundExecutorService;
+
+ public PerBuyerBiddingRunner(
+ @NonNull AdBidGenerator adBidGenerator,
+ @NonNull ScheduledThreadPoolExecutor scheduledExecutor,
+ @NonNull ListeningExecutorService backgroundExecutorService) {
+ mAdBidGenerator = adBidGenerator;
+ mScheduledExecutor = scheduledExecutor;
+ mBackgroundExecutorService = backgroundExecutorService;
+ }
+ /**
+ * This method executes bidding sequentially on the list of CustomAudience for a buyer. By
+ * leveraging the sequential executor, the bidding for subsequent Custom Audience is not even
+ * started until the previous bidding completes. This leads to significant saving of resources
+ * as without sequence, all the CAs begin bidding async and start downloading JS and consuming
+ * other resources. This ensures that at any point, only one bidding would be in progress.
+ *
+ * @param buyerTimeoutMs timeout value, post which incomplete CA bids are cancelled
+ * @param adSelectionConfig for the current Ad Selection
+ * @return list of futures with bidding outcomes
+ */
+ public List<ListenableFuture<AdBiddingOutcome>> runBidding(
+ final AdTechIdentifier buyer,
+ final List<DBCustomAudience> customAudienceList,
+ final long buyerTimeoutMs,
+ final AdSelectionConfig adSelectionConfig) {
+ LogUtil.v(
+ "Running bid for #%d Custom Audiences for buyer: %s",
+ customAudienceList.size(), buyer);
+
+ /*
+ * We require a unique sequencer per buyer, as using a global sequencer enforces sequence
+ * across buyers, where a buyer can starve other buyers' CAs from bidding.
+ */
+ ExecutionSequencer sequencer = ExecutionSequencer.create();
+ List<ListenableFuture<AdBiddingOutcome>> buyerBiddingOutcomes =
+ customAudienceList.stream()
+ .map(
+ (customAudience) ->
+ sequencer.submitAsync(
+ () ->
+ runBiddingPerCA(
+ customAudience, adSelectionConfig),
+ mBackgroundExecutorService))
+ .collect(Collectors.toList());
+
+ eventuallyTimeoutIncompleteTasks(buyerTimeoutMs, buyerBiddingOutcomes);
+ return buyerBiddingOutcomes;
+ }
+
+ private ListenableFuture<AdBiddingOutcome> runBiddingPerCA(
+ @NonNull final DBCustomAudience customAudience,
+ @NonNull final AdSelectionConfig adSelectionConfig) {
+ LogUtil.v(String.format("Invoking bidding for CA: %s", customAudience.getName()));
+
+ // TODO(b/233239475) : Validate Buyer signals in Ad Selection Config
+ AdSelectionSignals buyerSignal =
+ Optional.ofNullable(
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(customAudience.getBuyer()))
+ .orElse(AdSelectionSignals.EMPTY);
+ return mAdBidGenerator.runAdBiddingPerCA(
+ customAudience,
+ adSelectionConfig.getAdSelectionSignals(),
+ buyerSignal,
+ AdSelectionSignals.EMPTY);
+ }
+
+ /**
+ * Instead of timing out entire list of future, we only cancel the ones which are not done. This
+ * helps preserve tasks that are already completed while freeing up resources from the tasks
+ * which maybe in progress or are yet to be scheduled by cancelling them.
+ *
+ * @param timeoutMs delay after which these tasks should be cancelled
+ * @param runningTasks potentially ongoing tasks, that need to be timed-out
+ */
+ private <T> void eventuallyTimeoutIncompleteTasks(
+ final long timeoutMs, List<ListenableFuture<T>> runningTasks) {
+ Runnable cancelOngoingTasks =
+ () -> {
+ int incompleteTaskCount = 0;
+ for (ListenableFuture<T> runningTask : runningTasks) {
+ // TODO(b/254176437): use Closing futures to free up resources
+ if (runningTask.cancel(true)) {
+ incompleteTaskCount++;
+ }
+ }
+ LogUtil.v(
+ "Total tasks: #%d, cancelled incomplete tasks: #%d",
+ runningTasks.size(), incompleteTaskCount);
+ };
+ mScheduledExecutor.schedule(cancelOngoingTasks, timeoutMs, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/ReportImpressionScriptEngine.java b/adservices/service-core/java/com/android/adservices/service/adselection/ReportImpressionScriptEngine.java
index 057ab74945..60d50bd687 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/ReportImpressionScriptEngine.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/ReportImpressionScriptEngine.java
@@ -67,7 +67,8 @@ public class ReportImpressionScriptEngine {
public static final String PER_BUYER_SIGNALS_ARG_NAME = "per_buyer_signals";
public static final String SIGNALS_FOR_BUYER_ARG_NAME = "signals_for_buyer";
public static final String CONTEXTUAL_SIGNALS_ARG_NAME = "contextual_signals";
- public static final String CUSTOM_AUDIENCE_SIGNALS_ARG_NAME = "custom_audience_signals";
+ public static final String CUSTOM_AUDIENCE_REPORTING_SIGNALS_ARG_NAME =
+ "custom_audience_reporting_signals";
public static final String AD_SELECTION_CONFIG_ARG_NAME = "ad_selection_config";
public static final String BID_ARG_NAME = "bid";
public static final String RENDER_URI_ARG_NAME = "render_uri";
@@ -171,8 +172,9 @@ public class ReportImpressionScriptEngine {
.add(jsonArg(SIGNALS_FOR_BUYER_ARG_NAME, signalsForBuyer.toString()))
.add(jsonArg(CONTEXTUAL_SIGNALS_ARG_NAME, contextualSignals.toString()))
.add(
- CustomAudienceBiddingSignalsArgument.asScriptArgument(
- CUSTOM_AUDIENCE_SIGNALS_ARG_NAME, customAudienceSignals))
+ CustomAudienceReportingSignalsArgument.asScriptArgument(
+ CUSTOM_AUDIENCE_REPORTING_SIGNALS_ARG_NAME,
+ customAudienceSignals))
.build();
return transform(
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/TrustedServerAdSelectionRunner.java b/adservices/service-core/java/com/android/adservices/service/adselection/TrustedServerAdSelectionRunner.java
new file mode 100644
index 0000000000..d67a80e101
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/TrustedServerAdSelectionRunner.java
@@ -0,0 +1,405 @@
+/*
+ * 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.common.AdSelectionSignals;
+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.AdSelectionEntryDao;
+import com.android.adservices.data.adselection.CustomAudienceSignals;
+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.AdServicesHttpsClient;
+import com.android.adservices.service.common.AppImportanceFilter;
+import com.android.adservices.service.common.FledgeAllowListsFilter;
+import com.android.adservices.service.common.FledgeAuthorizationFilter;
+import com.android.adservices.service.common.Throttler;
+import com.android.adservices.service.consent.ConsentManager;
+import com.android.adservices.service.devapi.CustomAudienceDevOverridesHelper;
+import com.android.adservices.service.devapi.DevContext;
+import com.android.adservices.service.proto.SellerFrontEndGrpc;
+import com.android.adservices.service.proto.SellerFrontendService.BuyerInput;
+import com.android.adservices.service.proto.SellerFrontendService.SelectWinningAdRequest;
+import com.android.adservices.service.proto.SellerFrontendService.SelectWinningAdRequest.SelectWinningAdRawRequest.ClientType;
+import com.android.adservices.service.proto.SellerFrontendService.SelectWinningAdResponse;
+import com.android.adservices.service.stats.AdServicesLogger;
+import com.android.adservices.service.stats.ApiServiceLatencyCalculator;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.UncheckedTimeoutException;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.time.Clock;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import io.grpc.Codec;
+import io.grpc.ManagedChannel;
+import io.grpc.okhttp.OkHttpChannelBuilder;
+
+/**
+ * Offload execution to Bidding & Auction services. Sends an umbrella request to the Seller Frontend
+ * Service.
+ */
+public class TrustedServerAdSelectionRunner extends AdSelectionRunner {
+ public static final String GZIP = new Codec.Gzip().getMessageEncoding(); // "gzip"
+
+ @NonNull private final JsFetcher mJsFetcher;
+
+ public TrustedServerAdSelectionRunner(
+ @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 ConsentManager consentManager,
+ @NonNull final AdServicesLogger adServicesLogger,
+ @NonNull final DevContext devContext,
+ @NonNull AppImportanceFilter appImportanceFilter,
+ @NonNull final Flags flags,
+ @NonNull final Supplier<Throttler> throttlerSupplier,
+ int callerUid,
+ @NonNull final FledgeAuthorizationFilter fledgeAuthorizationFilter,
+ @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter,
+ @NonNull final ApiServiceLatencyCalculator apiServiceLatencyCalculator) {
+ super(
+ context,
+ customAudienceDao,
+ adSelectionEntryDao,
+ lightweightExecutorService,
+ backgroundExecutorService,
+ scheduledExecutor,
+ consentManager,
+ adServicesLogger,
+ appImportanceFilter,
+ flags,
+ throttlerSupplier,
+ callerUid,
+ fledgeAuthorizationFilter,
+ fledgeAllowListsFilter,
+ apiServiceLatencyCalculator);
+
+ CustomAudienceDevOverridesHelper mCustomAudienceDevOverridesHelper =
+ new CustomAudienceDevOverridesHelper(devContext, customAudienceDao);
+ mJsFetcher =
+ new JsFetcher(
+ mBackgroundExecutorService,
+ mLightweightExecutorService,
+ mCustomAudienceDevOverridesHelper,
+ adServicesHttpsClient);
+ }
+
+ @VisibleForTesting
+ TrustedServerAdSelectionRunner(
+ @NonNull final Context context,
+ @NonNull final CustomAudienceDao customAudienceDao,
+ @NonNull final AdSelectionEntryDao adSelectionEntryDao,
+ @NonNull final ExecutorService lightweightExecutorService,
+ @NonNull final ExecutorService backgroundExecutorService,
+ @NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
+ @NonNull final ConsentManager consentManager,
+ @NonNull final AdSelectionIdGenerator adSelectionIdGenerator,
+ @NonNull Clock clock,
+ @NonNull final AdServicesLogger adServicesLogger,
+ @NonNull AppImportanceFilter appImportanceFilter,
+ @NonNull final Flags flags,
+ @NonNull final Supplier<Throttler> throttlerSupplier,
+ int callerUid,
+ @NonNull final FledgeAuthorizationFilter fledgeAuthorizationFilter,
+ @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter,
+ @NonNull final JsFetcher jsFetcher,
+ @NonNull final ApiServiceLatencyCalculator apiServiceLatencyCalculator) {
+ super(
+ context,
+ customAudienceDao,
+ adSelectionEntryDao,
+ lightweightExecutorService,
+ backgroundExecutorService,
+ scheduledExecutor,
+ consentManager,
+ adSelectionIdGenerator,
+ clock,
+ adServicesLogger,
+ appImportanceFilter,
+ flags,
+ throttlerSupplier,
+ callerUid,
+ fledgeAuthorizationFilter,
+ fledgeAllowListsFilter,
+ apiServiceLatencyCalculator);
+
+ this.mJsFetcher = jsFetcher;
+ }
+
+ /** Prepares request and calls Seller Front-end Service to orchestrate ad selection. */
+ public ListenableFuture<AdSelectionOrchestrationResult> orchestrateAdSelection(
+ @NonNull final AdSelectionConfig adSelectionConfig,
+ @NonNull final String callerPackageName,
+ @NonNull ListenableFuture<List<DBCustomAudience>> buyersCustomAudiences) {
+
+ Function<List<DBCustomAudience>, Map<String, BuyerInput>> createBuyerInputs =
+ buyerCAs -> {
+ return createBuyerInputs(buyerCAs, adSelectionConfig);
+ };
+
+ Function<Map<String, BuyerInput>, SelectWinningAdRequest> createSelectWinningAdRequest =
+ encryptedInputPerBuyer -> {
+ return createSelectWinningAdRequest(adSelectionConfig, encryptedInputPerBuyer);
+ };
+
+ AsyncFunction<SelectWinningAdRequest, SelectWinningAdResponse> callSelectWinningAd =
+ req -> {
+ return callSelectWinningAd(req);
+ };
+
+ // Return the DBCustomAudience to fetch the buyerLogicJs in the next future.
+ Function<SelectWinningAdResponse, Pair<DBAdSelection.Builder, DBCustomAudience>>
+ getCustomAudienceAndDBAdSelection =
+ selectWinningAdResponse -> {
+ return getCustomAudienceAndDBAdSelection(
+ selectWinningAdResponse,
+ callerPackageName,
+ buyersCustomAudiences);
+ };
+
+ // TODO(b/254066067): Confirm if buyer logic for reporting can be fetched after rendering.
+ AsyncFunction<
+ Pair<DBAdSelection.Builder, DBCustomAudience>,
+ Pair<DBAdSelection.Builder, FluentFuture<String>>>
+ fetchBuyerLogicJs =
+ dbAdSelectionAndCAPair -> {
+ return fetchBuyerLogicJs(dbAdSelectionAndCAPair);
+ };
+
+ Function<Pair<DBAdSelection.Builder, FluentFuture<String>>, AdSelectionOrchestrationResult>
+ createAdSelectionResult =
+ dbAdSelectionAndBuyerLogicJsPair -> {
+ return createAdSelectionResult(dbAdSelectionAndBuyerLogicJsPair);
+ };
+
+ return FluentFuture.from(buyersCustomAudiences)
+ .transform(createBuyerInputs, mLightweightExecutorService)
+ .transform(createSelectWinningAdRequest, mLightweightExecutorService)
+ .transformAsync(callSelectWinningAd, mBackgroundExecutorService)
+ .transform(getCustomAudienceAndDBAdSelection, mLightweightExecutorService)
+ .transformAsync(fetchBuyerLogicJs, mBackgroundExecutorService)
+ .transform(createAdSelectionResult, mLightweightExecutorService)
+ .withTimeout(
+ mFlags.getAdSelectionOffDeviceOverallTimeoutMs(),
+ TimeUnit.MILLISECONDS,
+ mScheduledExecutor)
+ .catching(
+ TimeoutException.class,
+ this::handleTimeoutError,
+ mLightweightExecutorService);
+ }
+
+ private Map<String, BuyerInput> createBuyerInputs(
+ List<DBCustomAudience> buyerCAs, AdSelectionConfig adSelectionConfig) {
+ Map<String, BuyerInput> buyerInputs = new HashMap<>();
+ for (DBCustomAudience customAudience : buyerCAs) {
+ BuyerInput.CustomAudience.Builder customAudienceBuilder =
+ BuyerInput.CustomAudience.newBuilder()
+ .setName(customAudience.getName())
+ .addAllBiddingSignalsKeys(getBiddingSignalKeys(customAudience));
+
+ AdSelectionSignals perBuyerSignals =
+ adSelectionConfig.getPerBuyerSignals().get(customAudience.getBuyer());
+ BuyerInput input =
+ BuyerInput.newBuilder()
+ .addCustomAudiences(customAudienceBuilder)
+ .setBuyerSignals(convertSignalsToStruct(perBuyerSignals))
+ .build();
+ // TODO(b/254325545): Update the key to the domain of the BFE service, not buyer name.
+ buyerInputs.put(customAudience.getBuyer().toString(), input);
+ }
+
+ return buyerInputs;
+ }
+
+ private List<String> getBiddingSignalKeys(DBCustomAudience customAudience) {
+ List<String> biddingSignalKeys = customAudience.getTrustedBiddingData().getKeys();
+ // If the bidding signal keys is just the CA name, we don't need to pass it to the server.
+ if (biddingSignalKeys.size() == 1
+ && customAudience.getName().equals(biddingSignalKeys.get(0))) {
+ return ImmutableList.of();
+ }
+
+ // Remove the CA name from the bidding signal keys list to save space.
+ biddingSignalKeys.remove(customAudience.getName());
+ return biddingSignalKeys;
+ }
+
+ private SelectWinningAdRequest createSelectWinningAdRequest(
+ AdSelectionConfig adSelectionConfig, Map<String, BuyerInput> rawInputPerBuyer) {
+ SelectWinningAdRequest.SelectWinningAdRawRequest.AuctionConfig.Builder auctionConfig =
+ SelectWinningAdRequest.SelectWinningAdRawRequest.AuctionConfig.newBuilder()
+ .setSellerSignals(
+ convertSignalsToStruct((adSelectionConfig.getSellerSignals())))
+ // TODO(b/254068070): Check if this is contextually derived auction_signals.
+ .setAuctionSignals(
+ convertSignalsToStruct(adSelectionConfig.getAdSelectionSignals()));
+
+ SelectWinningAdRequest.SelectWinningAdRawRequest.Builder rawRequestBuilder =
+ SelectWinningAdRequest.SelectWinningAdRawRequest.newBuilder()
+ .setAdSelectionRequestId(mAdSelectionIdGenerator.generateId())
+ .putAllRawBuyerInput(rawInputPerBuyer)
+ .setAuctionConfig(auctionConfig)
+ // FLEDGE is currently only supported on GMS core devices.
+ .setClientType(ClientType.ANDROID);
+
+ return SelectWinningAdRequest.newBuilder().setRawRequest(rawRequestBuilder).build();
+ }
+
+ private ListenableFuture<SelectWinningAdResponse> callSelectWinningAd(
+ SelectWinningAdRequest req) {
+ // TODO(b/249575366): Pass in address + port when the fields are added.
+ ManagedChannel channel = OkHttpChannelBuilder.forAddress("localhost", 8080).build();
+ SellerFrontEndGrpc.SellerFrontEndFutureStub stub =
+ SellerFrontEndGrpc.newFutureStub(channel);
+
+ if (mFlags.getAdSelectionOffDeviceRequestCompressionEnabled()) {
+ stub = stub.withCompression(GZIP);
+ }
+
+ return stub.selectWinningAd(req);
+ }
+
+ private Pair<DBAdSelection.Builder, DBCustomAudience> getCustomAudienceAndDBAdSelection(
+ SelectWinningAdResponse selectWinningAdResponse,
+ String callerPackageName,
+ ListenableFuture<List<DBCustomAudience>> buyerCustomAudiences) {
+ SelectWinningAdResponse.SelectWinningAdRawResponse rawResponse =
+ selectWinningAdResponse.getRawResponse();
+ Uri winningAdRenderUri = Uri.parse(rawResponse.getAdRenderUrl());
+
+ // Find custom audience of the winning ad.
+ DBCustomAudience customAudience;
+ try {
+ // buyerCustomAudiences's future is already complete by the time this method is called.
+ List<DBCustomAudience> customAudiences = buyerCustomAudiences.get();
+ List<DBCustomAudience> filteredCustomAudiences =
+ customAudiences.stream()
+ .filter(
+ audience ->
+ audience.getName()
+ .equals(rawResponse.getCustomAudienceName()))
+ .collect(Collectors.toList());
+ customAudience = Iterables.getOnlyElement(filteredCustomAudiences);
+ } catch (InterruptedException | ExecutionException e) {
+ // Will never be thrown since the future has already completed for the code to be here.
+ throw new RuntimeException("Could not read buyerCustomAudiences list from device");
+ } catch (NoSuchElementException e) {
+ throw new IllegalStateException(
+ "Could not find corresponding custom audience returned from Bidding & Auction"
+ + " services");
+ }
+
+ CustomAudienceSignals customAudienceSignals =
+ CustomAudienceSignals.buildFromCustomAudience(customAudience);
+ DBAdSelection.Builder builder =
+ new DBAdSelection.Builder()
+ .setWinningAdBid(rawResponse.getBidPrice())
+ .setWinningAdRenderUri(winningAdRenderUri)
+ .setCustomAudienceSignals(customAudienceSignals)
+ .setBiddingLogicUri(customAudience.getBiddingLogicUri())
+ .setContextualSignals("{}")
+ .setCallerPackageName(callerPackageName);
+
+ return new Pair(builder, customAudience);
+ }
+
+ private ListenableFuture<Pair<DBAdSelection.Builder, FluentFuture<String>>> fetchBuyerLogicJs(
+ Pair<DBAdSelection.Builder, DBCustomAudience> dbAdSelectionAndCAPair) {
+ return mBackgroundExecutorService.submit(
+ () -> {
+ DBCustomAudience customAudience = dbAdSelectionAndCAPair.second;
+ FluentFuture<String> buyerDecisionLogic =
+ mJsFetcher.getBuyerDecisionLogic(
+ customAudience.getBiddingLogicUri(),
+ customAudience.getOwner(),
+ customAudience.getBuyer(),
+ customAudience.getName());
+ return new Pair(dbAdSelectionAndCAPair.first, buyerDecisionLogic);
+ });
+ }
+
+ private AdSelectionOrchestrationResult createAdSelectionResult(
+ Pair<DBAdSelection.Builder, FluentFuture<String>> dbAdSelectionAndBuyerLogicJsPair) {
+ try {
+ String buyerJsLogic = dbAdSelectionAndBuyerLogicJsPair.second.get();
+ return new AdSelectionOrchestrationResult(
+ dbAdSelectionAndBuyerLogicJsPair.first, buyerJsLogic);
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException("Could not fetch buyerJsLogic", e);
+ }
+ }
+
+ private Struct convertSignalsToStruct(AdSelectionSignals adSelectionSignals) {
+ Struct.Builder signals = Struct.newBuilder();
+ try {
+ JSONObject json = new JSONObject(adSelectionSignals.toString());
+ for (String keyStr : json.keySet()) {
+ Object obj = json.get(keyStr);
+ if (obj instanceof String) {
+ signals.putFields(
+ keyStr, Value.newBuilder().setStringValue((String) obj).build());
+ }
+ }
+ } catch (JSONException e) {
+ String error = "Invalid JSON found during SelectWinningAdRequest construction";
+ throw new IllegalArgumentException(error, e);
+ }
+
+ return signals.build();
+ }
+
+ @Nullable
+ private AdSelectionOrchestrationResult handleTimeoutError(TimeoutException e) {
+ LogUtil.e(e, "Ad Selection exceeded time limit");
+ throw new UncheckedTimeoutException(AD_SELECTION_TIMED_OUT);
+ }
+}