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