blob: 4935f6cc724d3f802a80fe41042a2afb45c4961b [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.adservices.adselection;
import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE;
import android.adservices.common.AdServicesStatusUtils;
import android.adservices.common.CallerMetadata;
import android.adservices.common.FledgeErrorResponse;
import android.adservices.common.SandboxedSdkContextUtils;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.app.sdksandbox.SandboxedSdkContext;
import android.content.Context;
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 java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
/**
* AdSelection Manager provides APIs for app and ad-SDKs to run ad selection processes as well as
* report impressions.
*/
// TODO(b/269798827): Enable for R.
@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";
@NonNull private Context mContext;
@NonNull private ServiceBinder<AdSelectionService> mServiceBinder;
/**
* 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);
}
/**
* Create AdSelectionManager
*
* @hide
*/
public AdSelectionManager(@NonNull Context context) {
Objects.requireNonNull(context);
// 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}.
*
* <p>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);
return this;
}
@NonNull
public TestAdSelectionManager getTestAdSelectionManager() {
return new TestAdSelectionManager(this);
}
@NonNull
AdSelectionService getService() {
return mServiceBinder.getService();
}
/**
* Runs the ad selection process on device to select a remarketing ad for the caller
* application.
*
* <p>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}.
*
* <p>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.
*
* <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
* the API received to run the ad selection.
*
* <p>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.
*
* <p>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.
*
* <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
* exceeds the allowed rate limits and is throttled.
*
* <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
* or permission is not requested.
*/
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void selectAds(
@NonNull AdSelectionConfig adSelectionConfig,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<AdSelectionOutcome, Exception> receiver) {
Objects.requireNonNull(adSelectionConfig);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
try {
final AdSelectionService service = 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.
*
* <p>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}.
*
* <p>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.
*
* <p>The input {@code adSelectionFromOutcomesConfig} contains:
*
* <ul>
* <li>{@code Seller} is required to be a registered {@link
* android.adservices.common.AdTechIdentifier}. Otherwise, {@link IllegalStateException}
* will be thrown.
* <li>{@code List of ad selection ids} should exist and come from {@link
* AdSelectionManager#selectAds} calls originated from the same application. Otherwise,
* {@link IllegalArgumentException} for input validation will raise listing violating ad
* selection ids.
* <li>{@code Selection logic URI} that could follow either the HTTPS or Ad Selection Prebuilt
* schemas.
* <p>If the URI follows HTTPS schema then the host should match the {@code seller}.
* Otherwise, {@link IllegalArgumentException} will be thrown.
* <p>Prebuilt URIs are a way of substituting a generic pre-built logics for the required
* JavaScripts for {@code selectOutcome}. Prebuilt Uri for this endpoint should follow;
* <ul>
* <li>{@code
* ad-selection-prebuilt://ad-selection-from-outcomes/<name>?<script-generation-parameters>}
* </ul>
* <p>If an unsupported prebuilt URI is passed or prebuilt URI feature is disabled by the
* service then {@link IllegalArgumentException} will be thrown.
* <p>See {@link AdSelectionFromOutcomesConfig.Builder#setSelectionLogicUri} for supported
* {@code <name>} and required {@code <script-generation-parameters>}.
* </ul>
*
* <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
* the API received to run the ad selection.
*
* <p>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.
*
* <p>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.
*
* <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
* exceeds the allowed rate limits and is throttled.
*
* <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
* or permission is not requested.
*
* @hide
*/
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void selectAds(
@NonNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<AdSelectionOutcome, Exception> receiver) {
Objects.requireNonNull(adSelectionFromOutcomesConfig);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
try {
final AdSelectionService service = 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.
*
* <p>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.
*
* <p>The function definition of {@code reportResult} is:
*
* <p>{@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 } }; } }
*
* <p>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.
*
* <p>The function definition of {@code reportWin} is:
*
* <p>{@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 } }; } }
*
* <p>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.
*
* <p>The function definition of {@code registerBeacon} is:
*
* <p>{@code function registerAdBeacon(event_key, reporting_url) }
*
* <p>For each ad event a buyer/seller is interested in reports for, they would invoke {@code
* registerAdBeacon}, where {@code event_key} is an identifier for that specific event. This
* {@code event_key} should match {@link ReportEventRequest#getEventKey()} when the SDK invokes
* {@link #reportEvent}.
*
* <p>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.
*
* <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
* the API received to report the impression.
*
* <p>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.
*
* <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
* exceeds the allowed rate limits and is throttled.
*
* <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
* or permission is not requested.
*
* <p>Impressions will be reported at most once as a best-effort attempt.
*/
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void reportImpression(
@NonNull ReportImpressionRequest request,
@NonNull Executor executor,
@NonNull OutcomeReceiver<Object, Exception> receiver) {
Objects.requireNonNull(request);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
try {
final AdSelectionService service = 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}. There is no guarantee about when the ad
* event will be reported. The event reporting could be delayed and reports could be batched.
*
* <p>Using {@link ReportEventRequest#getEventKey()}, 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#getEventData()} 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}.
*
* <p>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.
*
* <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
* the API received to report the ad event.
*
* <p>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.
*
* <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
* exceeds the allowed rate limits and is throttled.
*
* <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
* or permission is not requested.
*
* <p>Events will be reported at most once as a best-effort attempt.
*
* @hide
*/
// TODO(b/261812140): Unhide for report interaction API review
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void reportEvent(
@NonNull ReportEventRequest request,
@NonNull Executor executor,
@NonNull OutcomeReceiver<Object, Exception> receiver) {
Objects.requireNonNull(request);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
try {
final AdSelectionService service = getService();
service.reportInteraction(
new ReportInteractionInput.Builder()
.setAdSelectionId(request.getAdSelectionId())
.setInteractionKey(request.getEventKey())
.setInteractionData(request.getEventData())
.setReportingDestinations(request.getReportingDestinations())
.setCallerPackageName(getCallerPackageName())
.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.
*
* <p>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}.
*
* <p>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.
*
* <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
* the API received.
*
* <p>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.
*
* <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
* exceeds the allowed rate limits and is throttled.
*
* <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
* or permission is not requested.
*
* @hide
*/
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void setAppInstallAdvertisers(
@NonNull SetAppInstallAdvertisersRequest request,
@NonNull Executor executor,
@NonNull OutcomeReceiver<Object, Exception> receiver) {
Objects.requireNonNull(request);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
try {
final AdSelectionService service = 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)}.
*
* <p>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.
*
* <p>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.
*
* <p>A {@link SecurityException} is returned via the {@code outcomeReceiver} if:
*
* <ol>
* <li>the app has not declared the correct permissions in its manifest, or
* <li>the app or entity identified by the {@code callerAdTechIdentifier} are not authorized
* to use the API.
* </ol>
*
* An {@link IllegalStateException} is returned via the {@code outcomeReceiver} if the call does
* not come from an app with a foreground activity.
*
* <p>A {@link LimitExceededException} is returned via the {@code outcomeReceiver} if the call
* exceeds the calling app's API throttle.
*
* <p>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(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void updateAdCounterHistogram(
@NonNull UpdateAdCounterHistogramRequest updateAdCounterHistogramRequest,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Object, Exception> 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 = Objects.requireNonNull(getService());
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();
}
}