• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.adservices.customaudience;
18 
19 import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE;
20 
21 import static com.android.adservices.flags.Flags.FLAG_FLEDGE_SCHEDULE_CUSTOM_AUDIENCE_UPDATE_ENABLED;
22 
23 import android.adservices.common.AdServicesOutcomeReceiver;
24 import android.adservices.common.AdServicesStatusUtils;
25 import android.adservices.common.AdTechIdentifier;
26 import android.adservices.common.FledgeErrorResponse;
27 import android.adservices.common.SandboxedSdkContextUtils;
28 import android.annotation.CallbackExecutor;
29 import android.annotation.FlaggedApi;
30 import android.annotation.NonNull;
31 import android.annotation.RequiresPermission;
32 import android.app.sdksandbox.SandboxedSdkContext;
33 import android.content.Context;
34 import android.os.Build;
35 import android.os.LimitExceededException;
36 import android.os.OutcomeReceiver;
37 import android.os.RemoteException;
38 
39 import androidx.annotation.RequiresApi;
40 
41 import com.android.adservices.AdServicesCommon;
42 import com.android.adservices.LoggerFactory;
43 import com.android.adservices.ServiceBinder;
44 
45 import java.util.Objects;
46 import java.util.concurrent.Executor;
47 
48 /** CustomAudienceManager provides APIs for app and ad-SDKs to join / leave custom audiences. */
49 @RequiresApi(Build.VERSION_CODES.S)
50 public class CustomAudienceManager {
51     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
52     /**
53      * Constant that represents the service name for {@link CustomAudienceManager} to be used in
54      * {@link android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers}
55      *
56      * @hide
57      */
58     public static final String CUSTOM_AUDIENCE_SERVICE = "custom_audience_service";
59 
60     @NonNull private Context mContext;
61     @NonNull private ServiceBinder<ICustomAudienceService> mServiceBinder;
62 
63     /**
64      * Factory method for creating an instance of CustomAudienceManager.
65      *
66      * @param context The {@link Context} to use
67      * @return A {@link CustomAudienceManager} instance
68      */
69     @NonNull
get(@onNull Context context)70     public static CustomAudienceManager get(@NonNull Context context) {
71         // On T+, context.getSystemService() does more than just call constructor.
72         return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
73                 ? context.getSystemService(CustomAudienceManager.class)
74                 : new CustomAudienceManager(context);
75     }
76 
77     /**
78      * Create a service binder CustomAudienceManager
79      *
80      * @hide
81      */
CustomAudienceManager(@onNull Context context)82     public CustomAudienceManager(@NonNull Context context) {
83         Objects.requireNonNull(context);
84 
85         // In case the CustomAudienceManager is initiated from inside a sdk_sandbox process the
86         // fields will be immediately rewritten by the initialize method below.
87         initialize(context);
88     }
89 
90     /**
91      * Initializes {@link CustomAudienceManager} with the given {@code context}.
92      *
93      * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context.
94      * For more information check the javadoc on the {@link
95      * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}.
96      *
97      * @hide
98      * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry
99      */
initialize(@onNull Context context)100     public CustomAudienceManager initialize(@NonNull Context context) {
101         Objects.requireNonNull(context);
102 
103         mContext = context;
104         mServiceBinder =
105                 ServiceBinder.getServiceBinder(
106                         context,
107                         AdServicesCommon.ACTION_CUSTOM_AUDIENCE_SERVICE,
108                         ICustomAudienceService.Stub::asInterface);
109         return this;
110     }
111 
112     /** Create a service with test-enabling APIs */
113     @NonNull
getTestCustomAudienceManager()114     public TestCustomAudienceManager getTestCustomAudienceManager() {
115         return new TestCustomAudienceManager(this, getCallerPackageName());
116     }
117 
118     @NonNull
getService()119     ICustomAudienceService getService() {
120         ICustomAudienceService service = mServiceBinder.getService();
121         if (service == null) {
122             throw new IllegalStateException("custom audience service is not available.");
123         }
124         return service;
125     }
126 
127     /**
128      * Adds the user to the given {@link CustomAudience}.
129      *
130      * <p>An attempt to register the user for a custom audience with the same combination of {@code
131      * ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
132      * information to be overwritten, including the list of ads data.
133      *
134      * <p>Note that the ads list can be completely overwritten by the daily background fetch job.
135      *
136      * <p>This call fails with an {@link SecurityException} if
137      *
138      * <ol>
139      *   <li>the {@code ownerPackageName} is not calling app's package name and/or
140      *   <li>the buyer is not authorized to use the API.
141      * </ol>
142      *
143      * <p>This call fails with an {@link IllegalArgumentException} if
144      *
145      * <ol>
146      *   <li>the storage limit has been exceeded by the calling application and/or
147      *   <li>any URI parameters in the {@link CustomAudience} given are not authenticated with the
148      *       {@link CustomAudience} buyer.
149      * </ol>
150      *
151      * <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
152      * allowed rate limits and is throttled.
153      *
154      * <p>This call fails with an {@link IllegalStateException} if an internal service error is
155      * encountered.
156      */
157     @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
joinCustomAudience( @onNull JoinCustomAudienceRequest joinCustomAudienceRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Object, Exception> receiver)158     public void joinCustomAudience(
159             @NonNull JoinCustomAudienceRequest joinCustomAudienceRequest,
160             @NonNull @CallbackExecutor Executor executor,
161             @NonNull OutcomeReceiver<Object, Exception> receiver) {
162         Objects.requireNonNull(joinCustomAudienceRequest);
163         Objects.requireNonNull(executor);
164         Objects.requireNonNull(receiver);
165 
166         final CustomAudience customAudience = joinCustomAudienceRequest.getCustomAudience();
167 
168         try {
169             final ICustomAudienceService service = getService();
170 
171             service.joinCustomAudience(
172                     customAudience,
173                     getCallerPackageName(),
174                     new ICustomAudienceCallback.Stub() {
175                         @Override
176                         public void onSuccess() {
177                             executor.execute(() -> receiver.onResult(new Object()));
178                         }
179 
180                         @Override
181                         public void onFailure(FledgeErrorResponse failureParcel) {
182                             executor.execute(
183                                     () ->
184                                             receiver.onError(
185                                                     AdServicesStatusUtils.asException(
186                                                             failureParcel)));
187                         }
188                     });
189         } catch (RemoteException e) {
190             sLogger.e(e, "Exception");
191             receiver.onError(new IllegalStateException("Internal Error!", e));
192         }
193     }
194 
195     /**
196      * Adds the user to the {@link CustomAudience} fetched from a {@code fetchUri}.
197      *
198      * <p>An attempt to register the user for a custom audience with the same combination of {@code
199      * ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
200      * information to be overwritten, including the list of ads data.
201      *
202      * <p>Note that the ads list can be completely overwritten by the daily background fetch job.
203      *
204      * <p>This call fails with an {@link SecurityException} if
205      *
206      * <ol>
207      *   <li>the {@code ownerPackageName} is not calling app's package name and/or
208      *   <li>the buyer is not authorized to use the API.
209      * </ol>
210      *
211      * <p>This call fails with an {@link IllegalArgumentException} if
212      *
213      * <ol>
214      *   <li>the storage limit has been exceeded by the calling application and/or
215      *   <li>any URI parameters in the {@link CustomAudience} given are not authenticated with the
216      *       {@link CustomAudience} buyer.
217      * </ol>
218      *
219      * <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
220      * allowed rate limits and is throttled.
221      *
222      * <p>This call fails with an {@link IllegalStateException} if an internal service error is
223      * encountered.
224      */
225     @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
fetchAndJoinCustomAudience( @onNull FetchAndJoinCustomAudienceRequest fetchAndJoinCustomAudienceRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Object, Exception> receiver)226     public void fetchAndJoinCustomAudience(
227             @NonNull FetchAndJoinCustomAudienceRequest fetchAndJoinCustomAudienceRequest,
228             @NonNull @CallbackExecutor Executor executor,
229             @NonNull OutcomeReceiver<Object, Exception> receiver) {
230         Objects.requireNonNull(fetchAndJoinCustomAudienceRequest);
231         Objects.requireNonNull(executor);
232         Objects.requireNonNull(receiver);
233 
234         try {
235             final ICustomAudienceService service = getService();
236 
237             service.fetchAndJoinCustomAudience(
238                     new FetchAndJoinCustomAudienceInput.Builder(
239                                     fetchAndJoinCustomAudienceRequest.getFetchUri(),
240                                     getCallerPackageName())
241                             .setName(fetchAndJoinCustomAudienceRequest.getName())
242                             .setActivationTime(
243                                     fetchAndJoinCustomAudienceRequest.getActivationTime())
244                             .setExpirationTime(
245                                     fetchAndJoinCustomAudienceRequest.getExpirationTime())
246                             .setUserBiddingSignals(
247                                     fetchAndJoinCustomAudienceRequest.getUserBiddingSignals())
248                             .build(),
249                     new FetchAndJoinCustomAudienceCallback.Stub() {
250                         @Override
251                         public void onSuccess() {
252                             executor.execute(() -> receiver.onResult(new Object()));
253                         }
254 
255                         @Override
256                         public void onFailure(FledgeErrorResponse failureParcel) {
257                             executor.execute(
258                                     () ->
259                                             receiver.onError(
260                                                     AdServicesStatusUtils.asException(
261                                                             failureParcel)));
262                         }
263                     });
264         } catch (RemoteException e) {
265             sLogger.e(e, "Exception");
266             receiver.onError(new IllegalStateException("Internal Error!", e));
267         }
268     }
269 
270     /**
271      * Attempts to remove a user from a custom audience by deleting any existing {@link
272      * CustomAudience} data, identified by {@code ownerPackageName}, {@code buyer}, and {@code
273      * name}.
274      *
275      * <p>This call fails with an {@link SecurityException} if
276      *
277      * <ol>
278      *   <li>the {@code ownerPackageName} is not calling app's package name; and/or
279      *   <li>the buyer is not authorized to use the API.
280      * </ol>
281      *
282      * <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
283      * allowed rate limits and is throttled.
284      *
285      * <p>This call does not inform the caller whether the custom audience specified existed in
286      * on-device storage. In other words, it will fail silently when a buyer attempts to leave a
287      * custom audience that was not joined.
288      */
289     @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
leaveCustomAudience( @onNull LeaveCustomAudienceRequest leaveCustomAudienceRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Object, Exception> receiver)290     public void leaveCustomAudience(
291             @NonNull LeaveCustomAudienceRequest leaveCustomAudienceRequest,
292             @NonNull @CallbackExecutor Executor executor,
293             @NonNull OutcomeReceiver<Object, Exception> receiver) {
294         Objects.requireNonNull(leaveCustomAudienceRequest);
295         Objects.requireNonNull(executor);
296         Objects.requireNonNull(receiver);
297 
298         final AdTechIdentifier buyer = leaveCustomAudienceRequest.getBuyer();
299         final String name = leaveCustomAudienceRequest.getName();
300 
301         try {
302             final ICustomAudienceService service = getService();
303 
304             service.leaveCustomAudience(
305                     getCallerPackageName(),
306                     buyer,
307                     name,
308                     new ICustomAudienceCallback.Stub() {
309                         @Override
310                         public void onSuccess() {
311                             executor.execute(() -> receiver.onResult(new Object()));
312                         }
313 
314                         @Override
315                         public void onFailure(FledgeErrorResponse failureParcel) {
316                             executor.execute(
317                                     () ->
318                                             receiver.onError(
319                                                     AdServicesStatusUtils.asException(
320                                                             failureParcel)));
321                         }
322                     });
323         } catch (RemoteException e) {
324             sLogger.e(e, "Exception");
325             receiver.onError(new IllegalStateException("Internal Error!", e));
326         }
327     }
328 
329     /**
330      * Allows the API caller to schedule a deferred Custom Audience update. For each update the user
331      * will be able to join or leave a set of CustomAudiences.
332      *
333      * <p>This API only guarantees minimum delay to make the update, and does not guarantee a
334      * maximum deadline within which the update request would be made. Scheduled updates could be
335      * batched and queued together to preserve system resources, thus exact delay time is not
336      * guaranteed.
337      *
338      * <p>If the provided {@code shouldReplacePendingUpdates} is true, all the currently scheduled
339      * pending updates matching the {@code owner} i.e. calling app and {@code buyer} inferred from
340      * Update Uri will be deleted.
341      *
342      * <p>In order to conserve system resources the API will make and update request only if the
343      * following constraints are satisfied
344      *
345      * <ol>
346      *   <li>The device is using an un-metered internet connection
347      *   <li>The device battery is not low
348      *   <li>The device storage is not low
349      * </ol>
350      *
351      * <p>When the deferred update is triggered the API makes a POST request to the provided
352      * updateUri with the request body containing a JSON of Partial Custom Audience list.
353      *
354      * <p>An example of request body containing list of Partial Custom Audiences would look like:
355      *
356      * <pre>{@code
357      * {
358      *     "partial_custom_audience_data": [
359      *         {
360      *             "name": "running_shoes",
361      *             "activation_time": 1644375856883,
362      *             "expiration_time": 1644375908397
363      *         },
364      *         {
365      *             "name": "casual_shirt",
366      *             "user_bidding_signals": {
367      *                 "signal1": "value1"
368      *             }
369      *         }
370      *     ]
371      * }
372      * }</pre>
373      *
374      * <p>In response the API expects a JSON in return with following keys:
375      *
376      * <ol>
377      *   <li>"join" : Should contain list containing full data for a {@link CustomAudience} object
378      *   <li>"leave" : List of {@link CustomAudience} names that user is intended to be removed from
379      * </ol>
380      *
381      * <p>An example of JSON in response would look like:
382      *
383      * <pre>{@code
384      * {
385      *     "join": [
386      *         {
387      *             "name": "running-shoes",
388      *             "activation_time": 1680603133,
389      *             "expiration_time": 1680803133,
390      *             "user_bidding_signals": {
391      *                 "signal1": "value"
392      *             },
393      *             "trusted_bidding_data": {
394      *                 "trusted_bidding_uri": "https://example-dsp.com/",
395      *                 "trusted_bidding_keys": [
396      *                     "k1",
397      *                     "k2"
398      *                 ]
399      *             },
400      *             "bidding_logic_uri": "https://example-dsp.com/...",
401      *             "ads": [
402      *                 {
403      *                     "render_uri": "https://example-dsp.com/...",
404      *                     "metadata": {},
405      *                     "ad_filters": {
406      *                         "frequency_cap": {
407      *                             "win": [
408      *                                 {
409      *                                     "ad_counter_key": "key1",
410      *                                     "max_count": 2,
411      *                                     "interval_in_seconds": 60
412      *                                 }
413      *                             ],
414      *                             "view": [
415      *                                 {
416      *                                     "ad_counter_key": "key2",
417      *                                     "max_count": 10,
418      *                                     "interval_in_seconds": 3600
419      *                                 }
420      *                             ]
421      *                         },
422      *                         "app_install": {
423      *                             "package_names": [
424      *                                 "package.name.one"
425      *                             ]
426      *                         }
427      *                     }
428      *                 }
429      *             ]
430      *         },
431      *         {}
432      *     ],
433      *     "leave": [
434      *         "tennis_shoes",
435      *         "formal_shirt"
436      *     ]
437      * }
438      * }</pre>
439      *
440      * <p>An attempt to register the user for a custom audience from the same application with the
441      * same combination of {@code buyer} inferred from Update Uri, and {@code name} will cause the
442      * existing custom audience's information to be overwritten, including the list of ads data.
443      *
444      * <p>In case information related to any of the CustomAudience to be joined is malformed, the
445      * deferred update would silently ignore that single Custom Audience
446      *
447      * <p>When removing this API attempts to remove a user from a custom audience by deleting any
448      * existing {@link CustomAudience} identified by owner i.e. calling app, {@code buyer} inferred
449      * from Update Uri, and {@code name}
450      *
451      * <p>Any partial custom audience field set by the caller cannot be overridden by the custom
452      * audience fetched from the {@code updateUri}. Given multiple Custom Audiences could be
453      * returned by a buyer ad tech we will match the override restriction based on the names of the
454      * Custom Audiences. A buyer may skip returning a full Custom Audience for any Partial Custom
455      * Audience in request.
456      *
457      * <p>In case the API encounters transient errors while making the network call for update, like
458      * 5xx, connection timeout, rate limit exceeded it would employ retries, with backoff up to a
459      * 'retry limit' number of times. The API would also honor 'retry-after' header specifying the
460      * min amount of seconds by which the next request should be delayed.
461      *
462      * <p>In a scenario where server responds with a '429 status code', signifying 'Too many
463      * requests', API would place the deferred update and other updates for the same requester i.e.
464      * caller package and buyer combination, in a quarantine. The quarantine records would be
465      * referred before making any calls for requesters, and request will only be made once the
466      * quarantine period has expired. The applications can leverage the `retry-after` header to
467      * self-quarantine for traffic management to their servers and prevent being overwhelmed with
468      * requests. The default quarantine value will be set to 30 minutes.
469      *
470      * <p>This call fails with an {@link SecurityException} if
471      *
472      * <ol>
473      *   <li>the {@code ownerPackageName} is not calling app's package name; and/or
474      *   <li>the buyer, inferred from {@code updateUri}, is not authorized to use the API.
475      * </ol>
476      *
477      * <p>This call fails with an {@link IllegalArgumentException} if
478      *
479      * <ol>
480      *   <li>the provided {@code updateUri} is invalid or malformed.
481      *   <li>the provided {@code delayTime} is not within permissible bounds
482      *   <li>the combined size of {@code partialCustomAudience} list is larger than allowed limits
483      * </ol>
484      *
485      * <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
486      * allowed rate limits and is throttled.
487      *
488      * <p>This call fails with {@link IllegalStateException} if the provided {@code
489      * shouldReplacePendingUpdates} is false, and there exists a pending update in the queue.
490      */
491     @FlaggedApi(FLAG_FLEDGE_SCHEDULE_CUSTOM_AUDIENCE_UPDATE_ENABLED)
492     @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
scheduleCustomAudienceUpdate( @onNull ScheduleCustomAudienceUpdateRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull AdServicesOutcomeReceiver<Object, Exception> receiver)493     public void scheduleCustomAudienceUpdate(
494             @NonNull ScheduleCustomAudienceUpdateRequest request,
495             @NonNull @CallbackExecutor Executor executor,
496             @NonNull AdServicesOutcomeReceiver<Object, Exception> receiver) {
497         Objects.requireNonNull(request);
498         Objects.requireNonNull(executor);
499         Objects.requireNonNull(receiver);
500 
501         try {
502             final ICustomAudienceService service = getService();
503             service.scheduleCustomAudienceUpdate(
504                     new ScheduleCustomAudienceUpdateInput.Builder(
505                                     request.getUpdateUri(),
506                                     getCallerPackageName(),
507                                     request.getMinDelay(),
508                                     request.getPartialCustomAudienceList())
509                             .setShouldReplacePendingUpdates(request.shouldReplacePendingUpdates())
510                             .build(),
511                     new ScheduleCustomAudienceUpdateCallback.Stub() {
512                         @Override
513                         public void onSuccess() {
514                             executor.execute(() -> receiver.onResult(new Object()));
515                         }
516 
517                         @Override
518                         public void onFailure(FledgeErrorResponse failureParcel) {
519                             executor.execute(
520                                     () ->
521                                             receiver.onError(
522                                                     AdServicesStatusUtils.asException(
523                                                             failureParcel)));
524                         }
525                     });
526 
527         } catch (RemoteException e) {
528             sLogger.e(e, "Exception");
529             receiver.onError(new IllegalStateException("Internal Error!", e));
530         }
531     }
532 
533 
getCallerPackageName()534     private String getCallerPackageName() {
535         SandboxedSdkContext sandboxedSdkContext =
536                 SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
537         return sandboxedSdkContext == null
538                 ? mContext.getPackageName()
539                 : sandboxedSdkContext.getClientPackageName();
540     }
541 }
542