blob: 54e959e33c990860a1c4d9955bc228cc989d736c [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.customaudience;
import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE;
import static com.android.adservices.flags.Flags.FLAG_FLEDGE_SCHEDULE_CUSTOM_AUDIENCE_UPDATE_ENABLED;
import android.adservices.common.AdServicesOutcomeReceiver;
import android.adservices.common.AdServicesStatusUtils;
import android.adservices.common.AdTechIdentifier;
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.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 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;
/** CustomAudienceManager provides APIs for app and ad-SDKs to join / leave custom audiences. */
@RequiresApi(Build.VERSION_CODES.S)
public class CustomAudienceManager {
private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
/**
* Constant that represents the service name for {@link CustomAudienceManager} to be used in
* {@link android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers}
*
* @hide
*/
public static final String CUSTOM_AUDIENCE_SERVICE = "custom_audience_service";
@NonNull private Context mContext;
@NonNull private ServiceBinder<ICustomAudienceService> mServiceBinder;
/**
* Factory method for creating an instance of CustomAudienceManager.
*
* @param context The {@link Context} to use
* @return A {@link CustomAudienceManager} instance
*/
@NonNull
public static CustomAudienceManager 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(CustomAudienceManager.class)
: new CustomAudienceManager(context);
}
/**
* Create a service binder CustomAudienceManager
*
* @hide
*/
public CustomAudienceManager(@NonNull Context context) {
Objects.requireNonNull(context);
// In case the CustomAudienceManager is initiated from inside a sdk_sandbox process the
// fields will be immediately rewritten by the initialize method below.
initialize(context);
}
/**
* Initializes {@link CustomAudienceManager} 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 CustomAudienceManager initialize(@NonNull Context context) {
Objects.requireNonNull(context);
mContext = context;
mServiceBinder =
ServiceBinder.getServiceBinder(
context,
AdServicesCommon.ACTION_CUSTOM_AUDIENCE_SERVICE,
ICustomAudienceService.Stub::asInterface);
return this;
}
/** Create a service with test-enabling APIs */
@NonNull
public TestCustomAudienceManager getTestCustomAudienceManager() {
return new TestCustomAudienceManager(this, getCallerPackageName());
}
@NonNull
ICustomAudienceService getService() {
ICustomAudienceService service = mServiceBinder.getService();
if (service == null) {
throw new IllegalStateException("custom audience service is not available.");
}
return service;
}
/**
* Adds the user to the given {@link CustomAudience}.
*
* <p>An attempt to register the user for a custom audience with the same combination of {@code
* ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
* information to be overwritten, including the list of ads data.
*
* <p>Note that the ads list can be completely overwritten by the daily background fetch job.
*
* <p>This call fails with an {@link SecurityException} if
*
* <ol>
* <li>the {@code ownerPackageName} is not calling app's package name and/or
* <li>the buyer is not authorized to use the API.
* </ol>
*
* <p>This call fails with an {@link IllegalArgumentException} if
*
* <ol>
* <li>the storage limit has been exceeded by the calling application and/or
* <li>any URI parameters in the {@link CustomAudience} given are not authenticated with the
* {@link CustomAudience} buyer.
* </ol>
*
* <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
* allowed rate limits and is throttled.
*
* <p>This call fails with an {@link IllegalStateException} if an internal service error is
* encountered.
*/
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void joinCustomAudience(
@NonNull JoinCustomAudienceRequest joinCustomAudienceRequest,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Object, Exception> receiver) {
Objects.requireNonNull(joinCustomAudienceRequest);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
final CustomAudience customAudience = joinCustomAudienceRequest.getCustomAudience();
try {
final ICustomAudienceService service = getService();
service.joinCustomAudience(
customAudience,
getCallerPackageName(),
new ICustomAudienceCallback.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 (RemoteException e) {
sLogger.e(e, "Exception");
receiver.onError(new IllegalStateException("Internal Error!", e));
}
}
/**
* Adds the user to the {@link CustomAudience} fetched from a {@code fetchUri}.
*
* <p>An attempt to register the user for a custom audience with the same combination of {@code
* ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
* information to be overwritten, including the list of ads data.
*
* <p>Note that the ads list can be completely overwritten by the daily background fetch job.
*
* <p>This call fails with an {@link SecurityException} if
*
* <ol>
* <li>the {@code ownerPackageName} is not calling app's package name and/or
* <li>the buyer is not authorized to use the API.
* </ol>
*
* <p>This call fails with an {@link IllegalArgumentException} if
*
* <ol>
* <li>the storage limit has been exceeded by the calling application and/or
* <li>any URI parameters in the {@link CustomAudience} given are not authenticated with the
* {@link CustomAudience} buyer.
* </ol>
*
* <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
* allowed rate limits and is throttled.
*
* <p>This call fails with an {@link IllegalStateException} if an internal service error is
* encountered.
*/
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void fetchAndJoinCustomAudience(
@NonNull FetchAndJoinCustomAudienceRequest fetchAndJoinCustomAudienceRequest,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Object, Exception> receiver) {
Objects.requireNonNull(fetchAndJoinCustomAudienceRequest);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
try {
final ICustomAudienceService service = getService();
service.fetchAndJoinCustomAudience(
new FetchAndJoinCustomAudienceInput.Builder(
fetchAndJoinCustomAudienceRequest.getFetchUri(),
getCallerPackageName())
.setName(fetchAndJoinCustomAudienceRequest.getName())
.setActivationTime(
fetchAndJoinCustomAudienceRequest.getActivationTime())
.setExpirationTime(
fetchAndJoinCustomAudienceRequest.getExpirationTime())
.setUserBiddingSignals(
fetchAndJoinCustomAudienceRequest.getUserBiddingSignals())
.build(),
new FetchAndJoinCustomAudienceCallback.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 (RemoteException e) {
sLogger.e(e, "Exception");
receiver.onError(new IllegalStateException("Internal Error!", e));
}
}
/**
* Attempts to remove a user from a custom audience by deleting any existing {@link
* CustomAudience} data, identified by {@code ownerPackageName}, {@code buyer}, and {@code
* name}.
*
* <p>This call fails with an {@link SecurityException} if
*
* <ol>
* <li>the {@code ownerPackageName} is not calling app's package name; and/or
* <li>the buyer is not authorized to use the API.
* </ol>
*
* <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
* allowed rate limits and is throttled.
*
* <p>This call does not inform the caller whether the custom audience specified existed in
* on-device storage. In other words, it will fail silently when a buyer attempts to leave a
* custom audience that was not joined.
*/
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void leaveCustomAudience(
@NonNull LeaveCustomAudienceRequest leaveCustomAudienceRequest,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Object, Exception> receiver) {
Objects.requireNonNull(leaveCustomAudienceRequest);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
final AdTechIdentifier buyer = leaveCustomAudienceRequest.getBuyer();
final String name = leaveCustomAudienceRequest.getName();
try {
final ICustomAudienceService service = getService();
service.leaveCustomAudience(
getCallerPackageName(),
buyer,
name,
new ICustomAudienceCallback.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 (RemoteException e) {
sLogger.e(e, "Exception");
receiver.onError(new IllegalStateException("Internal Error!", e));
}
}
/**
* Allows the API caller to schedule a deferred Custom Audience update. For each update the user
* will be able to join or leave a set of CustomAudiences.
*
* <p>This API only guarantees minimum delay to make the update, and does not guarantee a
* maximum deadline within which the update request would be made. Scheduled updates could be
* batched and queued together to preserve system resources, thus exact delay time is not
* guaranteed.
*
* <p>In order to conserve system resources the API will make and update request only if the
* following constraints are satisfied
*
* <ol>
* <li>The device is using an un-metered internet connection
* <li>The device battery is not low
* <li>The device storage is not low
* </ol>
*
* <p>When the deferred update is triggered the API makes a POST request to the provided
* updateUri with the request body containing a JSON of Partial Custom Audience list.
*
* <p>An example of request body containing list of Partial Custom Audiences would look like:
*
* <pre>{@code
* {
* "partial_custom_audience_data": [
* {
* "name": "running_shoes",
* "activation_time": 1644375856883,
* "expiration_time": 1644375908397
* },
* {
* "name": "casual_shirt",
* "user_bidding_signals": {
* "signal1": "value1"
* }
* }
* ]
* }
* }</pre>
*
* <p>In response the API expects a JSON in return with following keys:
*
* <ol>
* <li>"join" : Should contain list containing full data for a {@link CustomAudience} object
* <li>"leave" : List of {@link CustomAudience} names that user is intended to be removed from
* </ol>
*
* <p>An example of JSON in response would look like:
*
* <pre>{@code
* {
* "join": [
* {
* "name": "running-shoes",
* "activation_time": 1680603133,
* "expiration_time": 1680803133,
* "user_bidding_signals": {
* "signal1": "value"
* },
* "trusted_bidding_data": {
* "trusted_bidding_uri": "https://example-dsp.com/",
* "trusted_bidding_keys": [
* "k1",
* "k2"
* ]
* },
* "bidding_logic_uri": "https://example-dsp.com/...",
* "ads": [
* {
* "render_uri": "https://example-dsp.com/...",
* "metadata": {},
* "ad_filters": {
* "frequency_cap": {
* "win": [
* {
* "ad_counter_key": "key1",
* "max_count": 2,
* "interval_in_seconds": 60
* }
* ],
* "view": [
* {
* "ad_counter_key": "key2",
* "max_count": 10,
* "interval_in_seconds": 3600
* }
* ]
* },
* "app_install": {
* "package_names": [
* "package.name.one"
* ]
* }
* }
* }
* ]
* },
* {}
* ],
* "leave": [
* "tennis_shoes",
* "formal_shirt"
* ]
* }
* }</pre>
*
* <p>An attempt to register the user for a custom audience from the same application with the
* same combination of {@code buyer} inferred from Update Uri, and {@code name} will cause the
* existing custom audience's information to be overwritten, including the list of ads data.
*
* <p>In case information related to any of the CustomAudience to be joined is malformed, the
* deferred update would silently ignore that single Custom Audience
*
* <p>When removing this API attempts to remove a user from a custom audience by deleting any
* existing {@link CustomAudience} identified by owner i.e. calling app, {@code buyer} inferred
* from Update Uri, and {@code name}
*
* <p>Any partial custom audience field set by the caller cannot be overridden by the custom
* audience fetched from the {@code updateUri}. Given multiple Custom Audiences could be
* returned by a DSP we will match the override restriction based on the names of the Custom
* Audiences. A DSP may skip returning a full Custom Audience for any Partial Custom Audience in
* request.
*
* <p>In case the API encounters transient errors while making the network call for update, like
* 5xx, connection timeout, rate limit exceeded it would employ retries, with backoff up to a
* 'retry limit' number of times. The API would also honor 'retry-after' header specifying the
* min amount of seconds by which the next request should be delayed.
*
* <p>In a scenario where server responds with a '429 status code', signifying 'Too many
* requests', API would place the deferred update and other updates for the same requester i.e.
* caller package and buyer combination, in a quarantine. The quarantine records would be
* referred before making any calls for requesters, and request will only be made once the
* quarantine period has expired. The applications can leverage the `retry-after` header to
* self-quarantine for traffic management to their servers and prevent being overwhelmed with
* requests. The default quarantine value will be set to 30 minutes.
*
* <p>This call fails with an {@link SecurityException} if
*
* <ol>
* <li>the {@code ownerPackageName} is not calling app's package name; and/or
* <li>the buyer, inferred from {@code updateUri}, is not authorized to use the API.
* </ol>
*
* <p>This call fails with an {@link IllegalArgumentException} if
*
* <ol>
* <li>the provided {@code updateUri} is invalid or malformed.
* <li>the provided {@code delayTime} is not within permissible bounds
* <li>the combined size of {@code partialCustomAudience} list is larger than allowed limits
* </ol>
*
* <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
* allowed rate limits and is throttled.
*/
@FlaggedApi(FLAG_FLEDGE_SCHEDULE_CUSTOM_AUDIENCE_UPDATE_ENABLED)
@RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
public void scheduleCustomAudienceUpdate(
@NonNull ScheduleCustomAudienceUpdateRequest request,
@NonNull @CallbackExecutor Executor executor,
@NonNull AdServicesOutcomeReceiver<Object, Exception> receiver) {
Objects.requireNonNull(request);
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
try {
final ICustomAudienceService service = getService();
service.scheduleCustomAudienceUpdate(
new ScheduleCustomAudienceUpdateInput.Builder(
request.getUpdateUri(),
getCallerPackageName(),
request.getMinDelay(),
request.getPartialCustomAudienceList())
.build(),
new ScheduleCustomAudienceUpdateCallback.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 (RemoteException e) {
sLogger.e(e, "Exception");
receiver.onError(new IllegalStateException("Internal Error!", e));
}
}
private String getCallerPackageName() {
SandboxedSdkContext sandboxedSdkContext =
SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
return sandboxedSdkContext == null
? mContext.getPackageName()
: sandboxedSdkContext.getClientPackageName();
}
}