| /* |
| * 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(); |
| } |
| } |