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>In order to conserve system resources the API will make and update request only if the
339      * following constraints are satisfied
340      *
341      * <ol>
342      *   <li>The device is using an un-metered internet connection
343      *   <li>The device battery is not low
344      *   <li>The device storage is not low
345      * </ol>
346      *
347      * <p>When the deferred update is triggered the API makes a POST request to the provided
348      * updateUri with the request body containing a JSON of Partial Custom Audience list.
349      *
350      * <p>An example of request body containing list of Partial Custom Audiences would look like:
351      *
352      * <pre>{@code
353      * {
354      *     "partial_custom_audience_data": [
355      *         {
356      *             "name": "running_shoes",
357      *             "activation_time": 1644375856883,
358      *             "expiration_time": 1644375908397
359      *         },
360      *         {
361      *             "name": "casual_shirt",
362      *             "user_bidding_signals": {
363      *                 "signal1": "value1"
364      *             }
365      *         }
366      *     ]
367      * }
368      * }</pre>
369      *
370      * <p>In response the API expects a JSON in return with following keys:
371      *
372      * <ol>
373      *   <li>"join" : Should contain list containing full data for a {@link CustomAudience} object
374      *   <li>"leave" : List of {@link CustomAudience} names that user is intended to be removed from
375      * </ol>
376      *
377      * <p>An example of JSON in response would look like:
378      *
379      * <pre>{@code
380      * {
381      *     "join": [
382      *         {
383      *             "name": "running-shoes",
384      *             "activation_time": 1680603133,
385      *             "expiration_time": 1680803133,
386      *             "user_bidding_signals": {
387      *                 "signal1": "value"
388      *             },
389      *             "trusted_bidding_data": {
390      *                 "trusted_bidding_uri": "https://example-dsp.com/",
391      *                 "trusted_bidding_keys": [
392      *                     "k1",
393      *                     "k2"
394      *                 ]
395      *             },
396      *             "bidding_logic_uri": "https://example-dsp.com/...",
397      *             "ads": [
398      *                 {
399      *                     "render_uri": "https://example-dsp.com/...",
400      *                     "metadata": {},
401      *                     "ad_filters": {
402      *                         "frequency_cap": {
403      *                             "win": [
404      *                                 {
405      *                                     "ad_counter_key": "key1",
406      *                                     "max_count": 2,
407      *                                     "interval_in_seconds": 60
408      *                                 }
409      *                             ],
410      *                             "view": [
411      *                                 {
412      *                                     "ad_counter_key": "key2",
413      *                                     "max_count": 10,
414      *                                     "interval_in_seconds": 3600
415      *                                 }
416      *                             ]
417      *                         },
418      *                         "app_install": {
419      *                             "package_names": [
420      *                                 "package.name.one"
421      *                             ]
422      *                         }
423      *                     }
424      *                 }
425      *             ]
426      *         },
427      *         {}
428      *     ],
429      *     "leave": [
430      *         "tennis_shoes",
431      *         "formal_shirt"
432      *     ]
433      * }
434      * }</pre>
435      *
436      * <p>An attempt to register the user for a custom audience from the same application with the
437      * same combination of {@code buyer} inferred from Update Uri, and {@code name} will cause the
438      * existing custom audience's information to be overwritten, including the list of ads data.
439      *
440      * <p>In case information related to any of the CustomAudience to be joined is malformed, the
441      * deferred update would silently ignore that single Custom Audience
442      *
443      * <p>When removing this API attempts to remove a user from a custom audience by deleting any
444      * existing {@link CustomAudience} identified by owner i.e. calling app, {@code buyer} inferred
445      * from Update Uri, and {@code name}
446      *
447      * <p>Any partial custom audience field set by the caller cannot be overridden by the custom
448      * audience fetched from the {@code updateUri}. Given multiple Custom Audiences could be
449      * returned by a DSP we will match the override restriction based on the names of the Custom
450      * Audiences. A DSP may skip returning a full Custom Audience for any Partial Custom Audience in
451      * request.
452      *
453      * <p>In case the API encounters transient errors while making the network call for update, like
454      * 5xx, connection timeout, rate limit exceeded it would employ retries, with backoff up to a
455      * 'retry limit' number of times. The API would also honor 'retry-after' header specifying the
456      * min amount of seconds by which the next request should be delayed.
457      *
458      * <p>In a scenario where server responds with a '429 status code', signifying 'Too many
459      * requests', API would place the deferred update and other updates for the same requester i.e.
460      * caller package and buyer combination, in a quarantine. The quarantine records would be
461      * referred before making any calls for requesters, and request will only be made once the
462      * quarantine period has expired. The applications can leverage the `retry-after` header to
463      * self-quarantine for traffic management to their servers and prevent being overwhelmed with
464      * requests. The default quarantine value will be set to 30 minutes.
465      *
466      * <p>This call fails with an {@link SecurityException} if
467      *
468      * <ol>
469      *   <li>the {@code ownerPackageName} is not calling app's package name; and/or
470      *   <li>the buyer, inferred from {@code updateUri}, is not authorized to use the API.
471      * </ol>
472      *
473      * <p>This call fails with an {@link IllegalArgumentException} if
474      *
475      * <ol>
476      *   <li>the provided {@code updateUri} is invalid or malformed.
477      *   <li>the provided {@code delayTime} is not within permissible bounds
478      *   <li>the combined size of {@code partialCustomAudience} list is larger than allowed limits
479      * </ol>
480      *
481      * <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
482      * allowed rate limits and is throttled.
483      */
484     @FlaggedApi(FLAG_FLEDGE_SCHEDULE_CUSTOM_AUDIENCE_UPDATE_ENABLED)
485     @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
scheduleCustomAudienceUpdate( @onNull ScheduleCustomAudienceUpdateRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull AdServicesOutcomeReceiver<Object, Exception> receiver)486     public void scheduleCustomAudienceUpdate(
487             @NonNull ScheduleCustomAudienceUpdateRequest request,
488             @NonNull @CallbackExecutor Executor executor,
489             @NonNull AdServicesOutcomeReceiver<Object, Exception> receiver) {
490         Objects.requireNonNull(request);
491         Objects.requireNonNull(executor);
492         Objects.requireNonNull(receiver);
493 
494         try {
495             final ICustomAudienceService service = getService();
496 
497             service.scheduleCustomAudienceUpdate(
498                     new ScheduleCustomAudienceUpdateInput.Builder(
499                                     request.getUpdateUri(),
500                                     getCallerPackageName(),
501                                     request.getMinDelay(),
502                                     request.getPartialCustomAudienceList())
503                             .build(),
504                     new ScheduleCustomAudienceUpdateCallback.Stub() {
505                         @Override
506                         public void onSuccess() {
507                             executor.execute(() -> receiver.onResult(new Object()));
508                         }
509 
510                         @Override
511                         public void onFailure(FledgeErrorResponse failureParcel) {
512                             executor.execute(
513                                     () ->
514                                             receiver.onError(
515                                                     AdServicesStatusUtils.asException(
516                                                             failureParcel)));
517                         }
518                     });
519 
520         } catch (RemoteException e) {
521             sLogger.e(e, "Exception");
522             receiver.onError(new IllegalStateException("Internal Error!", e));
523         }
524     }
525 
getCallerPackageName()526     private String getCallerPackageName() {
527         SandboxedSdkContext sandboxedSdkContext =
528                 SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
529         return sandboxedSdkContext == null
530                 ? mContext.getPackageName()
531                 : sandboxedSdkContext.getClientPackageName();
532     }
533 }
534