/* * 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 android.adservices.adselection; import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE; import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_SELECTION; import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_PROTECTED_SIGNALS; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.adservices.adid.AdId; import android.adservices.adid.AdIdCompatibleManager; import android.adservices.common.AdServicesStatusUtils; import android.adservices.common.AssetFileDescriptorUtil; import android.adservices.common.CallerMetadata; import android.adservices.common.FledgeErrorResponse; import android.adservices.common.SandboxedSdkContextUtils; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.app.sdksandbox.SandboxedSdkContext; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.os.Build; import android.os.LimitExceededException; import android.os.OutcomeReceiver; import android.os.RemoteException; import android.os.SystemClock; import android.os.TransactionTooLargeException; import androidx.annotation.RequiresApi; import com.android.adservices.AdServicesCommon; import com.android.adservices.LoggerFactory; import com.android.adservices.ServiceBinder; import com.android.adservices.flags.Flags; import com.android.internal.annotations.VisibleForTesting; import java.io.IOException; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; /** * AdSelection Manager provides APIs for app and ad-SDKs to run ad selection processes as well as * report impressions. */ @RequiresApi(Build.VERSION_CODES.S) public class AdSelectionManager { private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); /** * Constant that represents the service name for {@link AdSelectionManager} to be used in {@link * android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers} * * @hide */ public static final String AD_SELECTION_SERVICE = "ad_selection_service"; private static final long AD_ID_TIMEOUT_MS = 400; private static final String DEBUG_API_WARNING_MESSAGE = "To enable debug api, include ACCESS_ADSERVICES_AD_ID " + "permission and enable advertising ID under device settings"; private final Executor mAdIdExecutor = Executors.newCachedThreadPool(); @NonNull private Context mContext; @NonNull private ServiceBinder mServiceBinder; @NonNull private AdIdCompatibleManager mAdIdManager; @NonNull private ServiceProvider mServiceProvider; /** * Factory method for creating an instance of AdSelectionManager. * * @param context The {@link Context} to use * @return A {@link AdSelectionManager} instance */ @NonNull public static AdSelectionManager get(@NonNull Context context) { // On T+, context.getSystemService() does more than just call constructor. return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) ? context.getSystemService(AdSelectionManager.class) : new AdSelectionManager(context); } /** * Factory method for creating an instance of AdSelectionManager. * *

Note: This is for testing only. * * @param context The {@link Context} to use * @param adIdManager The {@link AdIdCompatibleManager} instance to use * @param adSelectionService The {@link AdSelectionService} instance to use * @return A {@link AdSelectionManager} instance * @hide */ @VisibleForTesting @NonNull public static AdSelectionManager get( @NonNull Context context, @NonNull AdIdCompatibleManager adIdManager, @NonNull AdSelectionService adSelectionService) { AdSelectionManager adSelectionManager = AdSelectionManager.get(context); adSelectionManager.mAdIdManager = adIdManager; adSelectionManager.mServiceProvider = () -> adSelectionService; return adSelectionManager; } /** * Create AdSelectionManager * * @hide */ public AdSelectionManager(@NonNull Context context) { Objects.requireNonNull(context); // Initialize the default service provider mServiceProvider = this::doGetService; // In case the AdSelectionManager is initiated from inside a sdk_sandbox process the // fields will be immediately rewritten by the initialize method below. initialize(context); } /** * Initializes {@link AdSelectionManager} with the given {@code context}. * *

This method is called by the {@link SandboxedSdkContext} to propagate the correct context. * For more information check the javadoc on the {@link * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}. * * @hide * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry */ public AdSelectionManager initialize(@NonNull Context context) { Objects.requireNonNull(context); mContext = context; mServiceBinder = ServiceBinder.getServiceBinder( context, AdServicesCommon.ACTION_AD_SELECTION_SERVICE, AdSelectionService.Stub::asInterface); mAdIdManager = new AdIdCompatibleManager(context); return this; } @NonNull public TestAdSelectionManager getTestAdSelectionManager() { return new TestAdSelectionManager(this); } /** * Using this interface {@code getService}'s implementation is decoupled from the default {@link * #doGetService()}. This allows us to inject mock instances of {@link AdSelectionService} to * inspect and test the manager-service boundary. */ interface ServiceProvider { @NonNull AdSelectionService getService(); } @NonNull ServiceProvider getServiceProvider() { return mServiceProvider; } @NonNull AdSelectionService doGetService() { return mServiceBinder.getService(); } /** * Collects custom audience data from device. Returns a compressed and encrypted blob to send to * auction servers for ad selection. For more details, please visit Bidding * and Auction Services Explainer. * *

Custom audience ads must have a {@code ad_render_id} to be eligible for to be collected. * *

See {@link AdSelectionManager#persistAdSelectionResult} for how to process the results of * the ad selection run on server-side with the blob generated by this API. * *

The output is passed by the receiver, which either returns an {@link * GetAdSelectionDataOutcome} for a successful run, or an {@link Exception} includes the type of * the exception thrown and the corresponding error message. * *

If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument * the API received to run the ad selection. * *

If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection * services.", it is caused by an internal failure of the ad selection service. * *

If the {@link TimeoutException} is thrown, it is caused when a timeout is encountered * during bidding, scoring, or overall selection process to find winning Ad. * *

If the {@link LimitExceededException} is thrown, it is caused when the calling package * exceeds the allowed rate limits and is throttled. * *

If the {@link SecurityException} is thrown, it is caused when the caller is not authorized * or permission is not requested. */ @RequiresPermission( anyOf = { ACCESS_ADSERVICES_CUSTOM_AUDIENCE, ACCESS_ADSERVICES_PROTECTED_SIGNALS, ACCESS_ADSERVICES_AD_SELECTION }) public void getAdSelectionData( @NonNull GetAdSelectionDataRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver receiver) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(receiver); try { final AdSelectionService service = getServiceProvider().getService(); service.getAdSelectionData( new GetAdSelectionDataInput.Builder() .setSeller(request.getSeller()) .setCallerPackageName(getCallerPackageName()) .setCoordinatorOriginUri(request.getCoordinatorOriginUri()) .build(), new CallerMetadata.Builder() .setBinderElapsedTimestamp(SystemClock.elapsedRealtime()) .build(), new GetAdSelectionDataCallback.Stub() { @Override public void onSuccess(GetAdSelectionDataResponse resultParcel) { executor.execute( () -> { byte[] adSelectionData; try { adSelectionData = getAdSelectionData(resultParcel); } catch (IOException e) { receiver.onError( new IllegalStateException( "Unable to return the AdSelectionData", e)); return; } receiver.onResult( new GetAdSelectionDataOutcome.Builder() .setAdSelectionId( resultParcel.getAdSelectionId()) .setAdSelectionData(adSelectionData) .build()); }); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> { receiver.onError( AdServicesStatusUtils.asException(failureParcel)); }); } }); } catch (NullPointerException e) { sLogger.e(e, "Unable to find the AdSelection service."); receiver.onError( new IllegalStateException("Unable to find the AdSelection service.", e)); } catch (RemoteException e) { sLogger.e(e, "Failure of AdSelection service."); receiver.onError(new IllegalStateException("Failure of AdSelection service.", e)); } } /** * Persists the ad selection results from the server-side. For more details, please visit Bidding * and Auction Services Explainer * *

See {@link AdSelectionManager#getAdSelectionData} for how to generate an encrypted blob to * run an ad selection on the server side. * *

The output is passed by the receiver, which either returns an {@link AdSelectionOutcome} * for a successful run, or an {@link Exception} includes the type of the exception thrown and * the corresponding error message. The {@link AdSelectionOutcome#getAdSelectionId()} is not * guaranteed to be the same as the {@link * PersistAdSelectionResultRequest#getAdSelectionDataId()} or the deprecated {@link * PersistAdSelectionResultRequest#getAdSelectionId()}. * *

If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument * the API received to run the ad selection. * *

If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection * services.", it is caused by an internal failure of the ad selection service. * *

If the {@link TimeoutException} is thrown, it is caused when a timeout is encountered * during bidding, scoring, or overall selection process to find winning Ad. * *

If the {@link LimitExceededException} is thrown, it is caused when the calling package * exceeds the allowed rate limits and is throttled. * *

If the {@link SecurityException} is thrown, it is caused when the caller is not authorized * or permission is not requested. */ @RequiresPermission( anyOf = { ACCESS_ADSERVICES_CUSTOM_AUDIENCE, ACCESS_ADSERVICES_PROTECTED_SIGNALS, ACCESS_ADSERVICES_AD_SELECTION }) public void persistAdSelectionResult( @NonNull PersistAdSelectionResultRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver receiver) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(receiver); try { final AdSelectionService service = getServiceProvider().getService(); service.persistAdSelectionResult( new PersistAdSelectionResultInput.Builder() .setSeller(request.getSeller()) .setAdSelectionId(request.getAdSelectionId()) .setAdSelectionResult(request.getAdSelectionResult()) .setCallerPackageName(getCallerPackageName()) .build(), new CallerMetadata.Builder() .setBinderElapsedTimestamp(SystemClock.elapsedRealtime()) .build(), new PersistAdSelectionResultCallback.Stub() { @Override public void onSuccess(PersistAdSelectionResultResponse resultParcel) { executor.execute( () -> receiver.onResult( new AdSelectionOutcome.Builder() .setAdSelectionId( resultParcel.getAdSelectionId()) .setRenderUri( resultParcel.getAdRenderUri()) .build())); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> { receiver.onError( AdServicesStatusUtils.asException(failureParcel)); }); } }); } catch (NullPointerException e) { sLogger.e(e, "Unable to find the AdSelection service."); receiver.onError( new IllegalStateException("Unable to find the AdSelection service.", e)); } catch (RemoteException e) { sLogger.e(e, "Failure of AdSelection service."); receiver.onError(new IllegalStateException("Failure of AdSelection service.", e)); } } /** * Runs the ad selection process on device to select a remarketing ad for the caller * application. * *

The input {@code adSelectionConfig} is provided by the Ads SDK and the {@link * AdSelectionConfig} object is transferred via a Binder call. For this reason, the total size * of these objects is bound to the Android IPC limitations. Failures to transfer the {@link * AdSelectionConfig} will throws an {@link TransactionTooLargeException}. * *

The input {@code adSelectionConfig} contains {@code Decision Logic Uri} that could follow * either the HTTPS or Ad Selection Prebuilt schemas. * *

If the URI follows HTTPS schema then the host should match the {@code seller}. Otherwise, * {@link IllegalArgumentException} will be thrown. * *

Prebuilt URIs are a way of substituting a generic pre-built logics for the required * JavaScripts for {@code scoreAds}. Prebuilt Uri for this endpoint should follow; * *

* *

If an unsupported prebuilt URI is passed or prebuilt URI feature is disabled by the * service then {@link IllegalArgumentException} will be thrown. * *

See {@link AdSelectionConfig.Builder#setDecisionLogicUri} for supported {@code } and * required {@code }. * *

The output is passed by the receiver, which either returns an {@link AdSelectionOutcome} * for a successful run, or an {@link Exception} includes the type of the exception thrown and * the corresponding error message. * *

If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument * the API received to run the ad selection. * *

If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection * services.", it is caused by an internal failure of the ad selection service. * *

If the {@link TimeoutException} is thrown, it is caused when a timeout is encountered * during bidding, scoring, or overall selection process to find winning Ad. * *

If the {@link LimitExceededException} is thrown, it is caused when the calling package * exceeds the allowed rate limits and is throttled. * *

If the {@link SecurityException} is thrown, it is caused when the caller is not authorized * or permission is not requested. */ @RequiresPermission( anyOf = { ACCESS_ADSERVICES_CUSTOM_AUDIENCE, ACCESS_ADSERVICES_PROTECTED_SIGNALS, ACCESS_ADSERVICES_AD_SELECTION }) public void selectAds( @NonNull AdSelectionConfig adSelectionConfig, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver receiver) { Objects.requireNonNull(adSelectionConfig); Objects.requireNonNull(executor); Objects.requireNonNull(receiver); try { final AdSelectionService service = getServiceProvider().getService(); service.selectAds( new AdSelectionInput.Builder() .setAdSelectionConfig(adSelectionConfig) .setCallerPackageName(getCallerPackageName()) .build(), new CallerMetadata.Builder() .setBinderElapsedTimestamp(SystemClock.elapsedRealtime()) .build(), new AdSelectionCallback.Stub() { @Override public void onSuccess(AdSelectionResponse resultParcel) { executor.execute( () -> receiver.onResult( new AdSelectionOutcome.Builder() .setAdSelectionId( resultParcel.getAdSelectionId()) .setRenderUri( resultParcel.getRenderUri()) .build())); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> { receiver.onError( AdServicesStatusUtils.asException(failureParcel)); }); } }); } catch (NullPointerException e) { sLogger.e(e, "Unable to find the AdSelection service."); receiver.onError( new IllegalStateException("Unable to find the AdSelection service.", e)); } catch (RemoteException e) { sLogger.e(e, "Failure of AdSelection service."); receiver.onError(new IllegalStateException("Failure of AdSelection service.", e)); } } /** * Selects an ad from the results of previously ran ad selections. * *

The input {@code adSelectionFromOutcomesConfig} is provided by the Ads SDK and the {@link * AdSelectionFromOutcomesConfig} object is transferred via a Binder call. For this reason, the * total size of these objects is bound to the Android IPC limitations. Failures to transfer the * {@link AdSelectionFromOutcomesConfig} will throws an {@link TransactionTooLargeException}. * *

The output is passed by the receiver, which either returns an {@link AdSelectionOutcome} * for a successful run, or an {@link Exception} includes the type of the exception thrown and * the corresponding error message. * *

The input {@code adSelectionFromOutcomesConfig} contains: * *

* *

If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument * the API received to run the ad selection. * *

If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection * services.", it is caused by an internal failure of the ad selection service. * *

If the {@link TimeoutException} is thrown, it is caused when a timeout is encountered * during bidding, scoring, or overall selection process to find winning Ad. * *

If the {@link LimitExceededException} is thrown, it is caused when the calling package * exceeds the allowed rate limits and is throttled. * *

If the {@link SecurityException} is thrown, it is caused when the caller is not authorized * or permission is not requested. */ @RequiresPermission( anyOf = { ACCESS_ADSERVICES_CUSTOM_AUDIENCE, ACCESS_ADSERVICES_PROTECTED_SIGNALS, ACCESS_ADSERVICES_AD_SELECTION }) public void selectAds( @NonNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver receiver) { Objects.requireNonNull(adSelectionFromOutcomesConfig); Objects.requireNonNull(executor); Objects.requireNonNull(receiver); try { final AdSelectionService service = getServiceProvider().getService(); service.selectAdsFromOutcomes( new AdSelectionFromOutcomesInput.Builder() .setAdSelectionFromOutcomesConfig(adSelectionFromOutcomesConfig) .setCallerPackageName(getCallerPackageName()) .build(), new CallerMetadata.Builder() .setBinderElapsedTimestamp(SystemClock.elapsedRealtime()) .build(), new AdSelectionCallback.Stub() { @Override public void onSuccess(AdSelectionResponse resultParcel) { executor.execute( () -> { if (resultParcel == null) { receiver.onResult(AdSelectionOutcome.NO_OUTCOME); } else { receiver.onResult( new AdSelectionOutcome.Builder() .setAdSelectionId( resultParcel.getAdSelectionId()) .setRenderUri( resultParcel.getRenderUri()) .build()); } }); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> { receiver.onError( AdServicesStatusUtils.asException(failureParcel)); }); } }); } catch (NullPointerException e) { sLogger.e(e, "Unable to find the AdSelection service."); receiver.onError( new IllegalStateException("Unable to find the AdSelection service.", e)); } catch (RemoteException e) { sLogger.e(e, "Failure of AdSelection service."); receiver.onError(new IllegalStateException("Failure of AdSelection service.", e)); } } /** * Notifies the service that there is a new impression to report for the ad selected by the * ad-selection run identified by {@code adSelectionId}. There is no guarantee about when the * impression will be reported. The impression reporting could be delayed and reports could be * batched. * *

To calculate the winning seller reporting URL, the service fetches the seller's JavaScript * logic from the {@link AdSelectionConfig#getDecisionLogicUri()} found at {@link * ReportImpressionRequest#getAdSelectionConfig()}. Then, the service executes one of the * functions found in the seller JS called {@code reportResult}, providing on-device signals as * well as {@link ReportImpressionRequest#getAdSelectionConfig()} as input parameters. * *

The function definition of {@code reportResult} is: * *

{@code function reportResult(ad_selection_config, render_url, bid, contextual_signals) { * return { 'status': status, 'results': {'signals_for_buyer': signals_for_buyer, * 'reporting_url': reporting_url } }; } } * *

To calculate the winning buyer reporting URL, the service fetches the winning buyer's * JavaScript logic which is fetched via the buyer's {@link * android.adservices.customaudience.CustomAudience#getBiddingLogicUri()}. Then, the service * executes one of the functions found in the buyer JS called {@code reportWin}, providing * on-device signals, {@code signals_for_buyer} calculated by {@code reportResult}, and specific * fields from {@link ReportImpressionRequest#getAdSelectionConfig()} as input parameters. * *

The function definition of {@code reportWin} is: * *

{@code function reportWin(ad_selection_signals, per_buyer_signals, signals_for_buyer, * contextual_signals, custom_audience_reporting_signals) { return {'status': 0, 'results': * {'reporting_url': reporting_url } }; } } * *

In addition, buyers and sellers have the option to register to receive reports on specific * ad events. To do so, they can invoke the platform provided {@code registerAdBeacon} function * inside {@code reportWin} and {@code reportResult} for buyers and sellers, respectively. * *

The function definition of {@code registerBeacon} is: * *

{@code function registerAdBeacon(beacons)}, where {@code beacons} is a dict of string to * string pairs * *

For each ad event a buyer/seller is interested in reports for, they would add an {@code * event_key}: {@code event_reporting_uri} pair to the {@code beacons} dict, where {@code * event_key} is an identifier for that specific event. This {@code event_key} should match * {@link ReportEventRequest#getKey()} when the SDK invokes {@link #reportEvent}. In addition, * each {@code event_reporting_uri} should parse properly into a {@link android.net.Uri}. This * will be the {@link android.net.Uri} reported to when the SDK invokes {@link #reportEvent}. * *

When the buyer/seller has added all the pairings they want to receive events for, they can * invoke {@code registerAdBeacon(beacons)}, where {@code beacons} is the name of the dict they * added the pairs to. * *

{@code registerAdBeacon} will throw a {@code TypeError} in these situations: * *

    *
  1. {@code registerAdBeacon}is called more than once. If this error is caught in * reportWin/reportResult, the original set of pairings will be registered *
  2. {@code registerAdBeacon} doesn't have exactly 1 dict argument. *
  3. The contents of the 1 dict argument are not all {@code String: String} pairings. *
* *

The output is passed by the {@code receiver}, which either returns an empty {@link Object} * for a successful run, or an {@link Exception} includes the type of the exception thrown and * the corresponding error message. * *

If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument * the API received to report the impression. * *

If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection * services.", it is caused by an internal failure of the ad selection service. * *

If the {@link LimitExceededException} is thrown, it is caused when the calling package * exceeds the allowed rate limits and is throttled. * *

If the {@link SecurityException} is thrown, it is caused when the caller is not authorized * or permission is not requested. * *

Impressions will be reported at most once as a best-effort attempt. */ @RequiresPermission( anyOf = { ACCESS_ADSERVICES_CUSTOM_AUDIENCE, ACCESS_ADSERVICES_PROTECTED_SIGNALS, ACCESS_ADSERVICES_AD_SELECTION }) public void reportImpression( @NonNull ReportImpressionRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver receiver) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(receiver); try { final AdSelectionService service = getServiceProvider().getService(); service.reportImpression( new ReportImpressionInput.Builder() .setAdSelectionId(request.getAdSelectionId()) .setAdSelectionConfig(request.getAdSelectionConfig()) .setCallerPackageName(getCallerPackageName()) .build(), new ReportImpressionCallback.Stub() { @Override public void onSuccess() { executor.execute(() -> receiver.onResult(new Object())); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> { receiver.onError( AdServicesStatusUtils.asException(failureParcel)); }); } }); } catch (NullPointerException e) { sLogger.e(e, "Unable to find the AdSelection service."); receiver.onError( new IllegalStateException("Unable to find the AdSelection service.", e)); } catch (RemoteException e) { sLogger.e(e, "Exception"); receiver.onError(new IllegalStateException("Failure of AdSelection service.", e)); } } /** * Notifies the service that there is a new ad event to report for the ad selected by the * ad-selection run identified by {@code adSelectionId}. An ad event is any occurrence that * happens to an ad associated with the given {@code adSelectionId}. There is no guarantee about * when the ad event will be reported. The event reporting could be delayed and reports could be * batched. * *

Using {@link ReportEventRequest#getKey()}, the service will fetch the {@code reportingUri} * that was registered in {@code registerAdBeacon}. See documentation of {@link * #reportImpression} for more details regarding {@code registerAdBeacon}. Then, the service * will attach {@link ReportEventRequest#getData()} to the request body of a POST request and * send the request. The body of the POST request will have the {@code content-type} of {@code * text/plain}, and the data will be transmitted in {@code charset=UTF-8}. * *

The output is passed by the receiver, which either returns an empty {@link Object} for a * successful run, or an {@link Exception} includes the type of the exception thrown and the * corresponding error message. * *

If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument * the API received to report the ad event. * *

If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection * services.", it is caused by an internal failure of the ad selection service. * *

If the {@link LimitExceededException} is thrown, it is caused when the calling package * exceeds the allowed rate limits and is throttled. * *

If the {@link SecurityException} is thrown, it is caused when the caller is not authorized * or permission is not requested. * *

Events will be reported at most once as a best-effort attempt. */ @RequiresPermission( anyOf = { ACCESS_ADSERVICES_CUSTOM_AUDIENCE, ACCESS_ADSERVICES_PROTECTED_SIGNALS, ACCESS_ADSERVICES_AD_SELECTION }) public void reportEvent( @NonNull ReportEventRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver receiver) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(receiver); try { ReportInteractionInput.Builder inputBuilder = new ReportInteractionInput.Builder() .setAdSelectionId(request.getAdSelectionId()) .setInteractionKey(request.getKey()) .setInteractionData(request.getData()) .setReportingDestinations(request.getReportingDestinations()) .setCallerPackageName(getCallerPackageName()) .setCallerSdkName(getCallerSdkName()) .setInputEvent(request.getInputEvent()); getAdId((adIdValue) -> inputBuilder.setAdId(adIdValue)); final AdSelectionService service = getServiceProvider().getService(); service.reportInteraction( inputBuilder.build(), new ReportInteractionCallback.Stub() { @Override public void onSuccess() { executor.execute(() -> receiver.onResult(new Object())); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> { receiver.onError( AdServicesStatusUtils.asException(failureParcel)); }); } }); } catch (NullPointerException e) { sLogger.e(e, "Unable to find the AdSelection service."); receiver.onError( new IllegalStateException("Unable to find the AdSelection service.", e)); } catch (RemoteException e) { sLogger.e(e, "Exception"); receiver.onError(new IllegalStateException("Failure of AdSelection service.", e)); } } /** * Gives the provided list of adtechs the ability to do app install filtering on the calling * app. * *

The input {@code request} is provided by the Ads SDK and the {@code request} object is * transferred via a Binder call. For this reason, the total size of these objects is bound to * the Android IPC limitations. Failures to transfer the {@code advertisers} will throws an * {@link TransactionTooLargeException}. * *

The output is passed by the receiver, which either returns an empty {@link Object} for a * successful run, or an {@link Exception} includes the type of the exception thrown and the * corresponding error message. * *

If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument * the API received. * *

If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection * services.", it is caused by an internal failure of the ad selection service. * *

If the {@link LimitExceededException} is thrown, it is caused when the calling package * exceeds the allowed rate limits and is throttled. * *

If the {@link SecurityException} is thrown, it is caused when the caller is not authorized * or permission is not requested. */ @FlaggedApi(Flags.FLAG_FLEDGE_AD_SELECTION_FILTERING_ENABLED) @RequiresPermission( anyOf = { ACCESS_ADSERVICES_CUSTOM_AUDIENCE, ACCESS_ADSERVICES_PROTECTED_SIGNALS, ACCESS_ADSERVICES_AD_SELECTION }) public void setAppInstallAdvertisers( @NonNull SetAppInstallAdvertisersRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver receiver) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(receiver); try { final AdSelectionService service = getServiceProvider().getService(); service.setAppInstallAdvertisers( new SetAppInstallAdvertisersInput.Builder() .setAdvertisers(request.getAdvertisers()) .setCallerPackageName(getCallerPackageName()) .build(), new SetAppInstallAdvertisersCallback.Stub() { @Override public void onSuccess() { executor.execute(() -> receiver.onResult(new Object())); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> { receiver.onError( AdServicesStatusUtils.asException(failureParcel)); }); } }); } catch (NullPointerException e) { sLogger.e(e, "Unable to find the AdSelection service."); receiver.onError( new IllegalStateException("Unable to find the AdSelection service.", e)); } catch (RemoteException e) { sLogger.e(e, "Exception"); receiver.onError(new IllegalStateException("Failure of AdSelection service.", e)); } } /** * Updates the counter histograms for an ad which was previously selected by a call to {@link * #selectAds(AdSelectionConfig, Executor, OutcomeReceiver)}. * *

The counter histograms are used in ad selection to inform frequency cap filtering on * candidate ads, where ads whose frequency caps are met or exceeded are removed from the * bidding process during ad selection. * *

Counter histograms can only be updated for ads specified by the given {@code * adSelectionId} returned by a recent call to FLEDGE ad selection from the same caller app. * *

A {@link SecurityException} is returned via the {@code outcomeReceiver} if: * *

    *
  1. the app has not declared the correct permissions in its manifest, or *
  2. the app or entity identified by the {@code callerAdTechIdentifier} are not authorized * to use the API. *
* * An {@link IllegalStateException} is returned via the {@code outcomeReceiver} if the call does * not come from an app with a foreground activity. * *

A {@link LimitExceededException} is returned via the {@code outcomeReceiver} if the call * exceeds the calling app's API throttle. * *

In all other failure cases, the {@code outcomeReceiver} will return an empty {@link * Object}. Note that to protect user privacy, internal errors will not be sent back via an * exception. */ @RequiresPermission( anyOf = { ACCESS_ADSERVICES_CUSTOM_AUDIENCE, ACCESS_ADSERVICES_PROTECTED_SIGNALS, ACCESS_ADSERVICES_AD_SELECTION }) public void updateAdCounterHistogram( @NonNull UpdateAdCounterHistogramRequest updateAdCounterHistogramRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver outcomeReceiver) { Objects.requireNonNull(updateAdCounterHistogramRequest, "Request must not be null"); Objects.requireNonNull(executor, "Executor must not be null"); Objects.requireNonNull(outcomeReceiver, "Outcome receiver must not be null"); try { final AdSelectionService service = getServiceProvider().getService(); Objects.requireNonNull(service); service.updateAdCounterHistogram( new UpdateAdCounterHistogramInput.Builder( updateAdCounterHistogramRequest.getAdSelectionId(), updateAdCounterHistogramRequest.getAdEventType(), updateAdCounterHistogramRequest.getCallerAdTech(), getCallerPackageName()) .build(), new UpdateAdCounterHistogramCallback.Stub() { @Override public void onSuccess() { executor.execute(() -> outcomeReceiver.onResult(new Object())); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> { outcomeReceiver.onError( AdServicesStatusUtils.asException(failureParcel)); }); } }); } catch (NullPointerException e) { sLogger.e(e, "Unable to find the AdSelection service"); outcomeReceiver.onError( new IllegalStateException("Unable to find the AdSelection service", e)); } catch (RemoteException e) { sLogger.e(e, "Remote exception encountered while updating ad counter histogram"); outcomeReceiver.onError(new IllegalStateException("Failure of AdSelection service", e)); } } private String getCallerPackageName() { SandboxedSdkContext sandboxedSdkContext = SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext); return sandboxedSdkContext == null ? mContext.getPackageName() : sandboxedSdkContext.getClientPackageName(); } private byte[] getAdSelectionData(GetAdSelectionDataResponse response) throws IOException { if (Objects.nonNull(response.getAssetFileDescriptor())) { AssetFileDescriptor assetFileDescriptor = response.getAssetFileDescriptor(); return AssetFileDescriptorUtil.readAssetFileDescriptorIntoBuffer(assetFileDescriptor); } else { return response.getAdSelectionData(); } } private String getCallerSdkName() { SandboxedSdkContext sandboxedSdkContext = SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext); return sandboxedSdkContext == null ? "" : sandboxedSdkContext.getSdkPackageName(); } private interface AdSelectionAdIdCallback { void onResult(@Nullable String adIdValue); } @SuppressLint("MissingPermission") private void getAdId(AdSelectionAdIdCallback adSelectionAdIdCallback) { try { CountDownLatch timer = new CountDownLatch(1); AtomicReference adIdValue = new AtomicReference<>(); mAdIdManager.getAdId( mAdIdExecutor, new android.adservices.common.AdServicesOutcomeReceiver<>() { @Override public void onResult(AdId adId) { String id = adId.getAdId(); adIdValue.set(!AdId.ZERO_OUT.equals(id) ? id : null); sLogger.v("AdId permission enabled: %b.", !AdId.ZERO_OUT.equals(id)); timer.countDown(); } @Override public void onError(Exception e) { if (e instanceof IllegalStateException || e instanceof SecurityException) { sLogger.w(DEBUG_API_WARNING_MESSAGE); } else { sLogger.w(e, DEBUG_API_WARNING_MESSAGE); } timer.countDown(); } }); boolean timedOut = false; try { timedOut = !timer.await(AD_ID_TIMEOUT_MS, MILLISECONDS); } catch (InterruptedException e) { sLogger.w(e, "Interrupted while getting the AdId."); } if (timedOut) { sLogger.w("AdId call timed out."); } adSelectionAdIdCallback.onResult(adIdValue.get()); } catch (Exception e) { sLogger.d(e, "Could not get AdId."); adSelectionAdIdCallback.onResult(null); } } }