diff options
Diffstat (limited to 'adservices/service-core/java/com/android/adservices/service/adselection')
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); + } +} |