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