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 com.android.tests.providers.sdkfledge; 18 19 import android.adservices.adselection.AdSelectionConfig; 20 import android.adservices.adselection.AdSelectionOutcome; 21 import android.adservices.adselection.AddAdSelectionOverrideRequest; 22 import android.adservices.adselection.ReportEventRequest; 23 import android.adservices.adselection.ReportImpressionRequest; 24 import android.adservices.adselection.UpdateAdCounterHistogramRequest; 25 import android.adservices.clients.adselection.AdSelectionClient; 26 import android.adservices.clients.adselection.TestAdSelectionClient; 27 import android.adservices.clients.customaudience.AdvertisingCustomAudienceClient; 28 import android.adservices.clients.customaudience.TestAdvertisingCustomAudienceClient; 29 import android.adservices.common.AdData; 30 import android.adservices.common.AdSelectionSignals; 31 import android.adservices.common.AdTechIdentifier; 32 import android.adservices.common.FrequencyCapFilters; 33 import android.adservices.customaudience.AddCustomAudienceOverrideRequest; 34 import android.adservices.customaudience.CustomAudience; 35 import android.adservices.customaudience.TrustedBiddingData; 36 import android.app.sdksandbox.LoadSdkException; 37 import android.app.sdksandbox.SandboxedSdk; 38 import android.app.sdksandbox.SandboxedSdkProvider; 39 import android.content.Context; 40 import android.net.Uri; 41 import android.os.Binder; 42 import android.os.Bundle; 43 import android.util.Log; 44 import android.view.View; 45 46 import androidx.annotation.NonNull; 47 48 import com.google.common.collect.ImmutableList; 49 import com.google.common.util.concurrent.MoreExecutors; 50 51 import java.time.Duration; 52 import java.time.Instant; 53 import java.time.temporal.ChronoUnit; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.concurrent.Executor; 59 import java.util.concurrent.Executors; 60 import java.util.concurrent.TimeUnit; 61 62 public class SdkFledge extends SandboxedSdkProvider { 63 private static final String TAG = "SdkFledge"; 64 private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool(); 65 66 private static final AdTechIdentifier SELLER = AdTechIdentifier.fromString("test.com"); 67 68 private static final AdTechIdentifier BUYER_1 = AdTechIdentifier.fromString("test2.com"); 69 private static final AdTechIdentifier BUYER_2 = AdTechIdentifier.fromString("test3.com"); 70 71 private static final String AD_URI_PREFIX = "/adverts/123/"; 72 73 private static final String SELLER_DECISION_LOGIC_URI_PATH = "/ssp/decision/logic/"; 74 private static final String BUYER_BIDDING_LOGIC_URI_PATH = "/buyer/bidding/logic/"; 75 private static final String SELLER_TRUSTED_SIGNAL_URI_PATH = "/kv/seller/signals/"; 76 77 private static final String SELLER_REPORTING_PATH = "/reporting/seller"; 78 private static final String BUYER_REPORTING_PATH = "/reporting/buyer"; 79 80 // Interaction reporting constants 81 private static final String CLICK_INTERACTION = "click"; 82 private static final String HOVER_INTERACTION = "hover"; 83 84 private static final String SELLER_CLICK_URI_PATH = "/click/seller"; 85 private static final String SELLER_HOVER_URI_PATH = "/hover/seller"; 86 87 private static final String BUYER_CLICK_URI_PATH = "/click/buyer"; 88 private static final String BUYER_HOVER_URI_PATH = "/hover/buyer"; 89 90 private static final String SELLER_CLICK_URI = 91 String.format("https://%s%s", SELLER, SELLER_CLICK_URI_PATH); 92 93 private static final String SELLER_HOVER_URI = 94 String.format("https://%s%s", SELLER, SELLER_HOVER_URI_PATH); 95 96 private static final String BUYER_1_CLICK_URI = 97 String.format("https://%s%s", BUYER_1, BUYER_CLICK_URI_PATH); 98 99 private static final String BUYER_1_HOVER_URI = 100 String.format("https://%s%s", BUYER_1, BUYER_HOVER_URI_PATH); 101 102 private static final String BUYER_2_CLICK_URI = 103 String.format("https://%s%s", BUYER_2, BUYER_CLICK_URI_PATH); 104 105 private static final String BUYER_2_HOVER_URI = 106 String.format("https://%s%s", BUYER_2, BUYER_HOVER_URI_PATH); 107 108 private static final AdSelectionSignals TRUSTED_SCORING_SIGNALS = 109 AdSelectionSignals.fromString( 110 "{\n" 111 + "\t\"render_uri_1\": \"signals_for_1\",\n" 112 + "\t\"render_uri_2\": \"signals_for_2\"\n" 113 + "}"); 114 115 private static final AdSelectionSignals TRUSTED_BIDDING_SIGNALS = 116 AdSelectionSignals.fromString( 117 "{\n" 118 + "\t\"example\": \"example\",\n" 119 + "\t\"valid\": \"Also valid\",\n" 120 + "\t\"list\": \"list\",\n" 121 + "\t\"of\": \"of\",\n" 122 + "\t\"keys\": \"trusted bidding signal Values\"\n" 123 + "}"); 124 125 private static final AdSelectionConfig AD_SELECTION_CONFIG = 126 anAdSelectionConfigBuilder() 127 .setCustomAudienceBuyers(Arrays.asList(BUYER_1, BUYER_2)) 128 .setDecisionLogicUri( 129 Uri.parse( 130 String.format( 131 "https://%s%s", 132 SELLER, SELLER_DECISION_LOGIC_URI_PATH))) 133 .setTrustedScoringSignalsUri( 134 Uri.parse( 135 String.format( 136 "https://%s%s", 137 SELLER, SELLER_TRUSTED_SIGNAL_URI_PATH))) 138 .build(); 139 private static final String HTTPS_SCHEME = "https"; 140 141 private static final int BUYER_DESTINATION = 142 ReportEventRequest.FLAG_REPORTING_DESTINATION_BUYER; 143 private static final int SELLER_DESTINATION = 144 ReportEventRequest.FLAG_REPORTING_DESTINATION_SELLER; 145 146 private static final String INTERACTION_DATA = "{\"key\":\"value\"}"; 147 148 private static final long SLEEP_TIME_MS = (long) 1500 + 100L; 149 150 private AdSelectionClient mAdSelectionClient; 151 private TestAdSelectionClient mTestAdSelectionClient; 152 private AdvertisingCustomAudienceClient mCustomAudienceClient; 153 private TestAdvertisingCustomAudienceClient mTestCustomAudienceClient; 154 155 @Override onLoadSdk(Bundle params)156 public SandboxedSdk onLoadSdk(Bundle params) throws LoadSdkException { 157 try { 158 setup(); 159 } catch (Exception e) { 160 String errorMessage = 161 String.format("Error setting up the test: message is %s", e.getMessage()); 162 Log.e(TAG, errorMessage); 163 throw new LoadSdkException(e, new Bundle()); 164 } 165 // TODO(b/274837158) Uncomment after API is un-hidden 166 // String decisionLogicJs = 167 // "function scoreAd(ad, bid, auction_config, seller_signals," 168 // + " trusted_scoring_signals, contextual_signal, user_signal," 169 // + " custom_audience_signal) { \n" 170 // + " return {'status': 0, 'score': bid };\n" 171 // + "}\n" 172 // + "function reportResult(ad_selection_config, render_uri, bid," 173 // + " contextual_signals) { \n" 174 // + " registerAdBeacon('click', '" 175 // + SELLER_CLICK_URI 176 // + "');\n" 177 // + " registerAdBeacon('hover', '" 178 // + SELLER_HOVER_URI 179 // + "');\n" 180 // + " return {'status': 0, 'results': {'signals_for_buyer':" 181 // + " '{\"signals_for_buyer\":1}', 'reporting_uri': '" 182 // + getUri(SELLER.toString(), SELLER_REPORTING_PATH).toString() 183 // + "' } };\n" 184 // + "}"; 185 186 String decisionLogicJs = 187 "function scoreAd(ad, bid, auction_config, seller_signals," 188 + " trusted_scoring_signals, contextual_signal, user_signal," 189 + " custom_audience_signal) { \n" 190 + " return {'status': 0, 'score': bid };\n" 191 + "}\n" 192 + "function reportResult(ad_selection_config, render_uri, bid," 193 + " contextual_signals) { \n" 194 + " return {'status': 0, 'results': {'signals_for_buyer':" 195 + " '{\"signals_for_buyer\":1}', 'reporting_uri': '" 196 + getUri(SELLER.toString(), SELLER_REPORTING_PATH).toString() 197 + "' } };\n" 198 + "}"; 199 200 201 // TODO(b/274837158) Uncomment after API is un-hidden 202 // String biddingLogicJsBuyer1 = 203 // "function generateBid(ad, auction_signals, per_buyer_signals," 204 // + " trusted_bidding_signals, contextual_signals," 205 // + " custom_audience_signals) { \n" 206 // + " return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };\n" 207 // + "}\n" 208 // + "function reportWin(ad_selection_signals, per_buyer_signals," 209 // + " signals_for_buyer, contextual_signals, custom_audience_signals) { \n" 210 // + " registerAdBeacon('click', '" 211 // + BUYER_1_CLICK_URI 212 // + "');\n" 213 // + " registerAdBeacon('hover', '" 214 // + BUYER_1_HOVER_URI 215 // + "');\n" 216 // + " return {'status': 0, 'results': {'reporting_uri': '" 217 // + getUri(BUYER_1.toString(), BUYER_REPORTING_PATH).toString() 218 // + "' } };\n" 219 // + "}"; 220 221 String biddingLogicJsBuyer1 = 222 "function generateBid(ad, auction_signals, per_buyer_signals," 223 + " trusted_bidding_signals, contextual_signals," 224 + " custom_audience_signals) { \n" 225 + " return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };\n" 226 + "}\n" 227 + "function reportWin(ad_selection_signals, per_buyer_signals," 228 + " signals_for_buyer, contextual_signals, custom_audience_signals) { \n" 229 + " return {'status': 0, 'results': {'reporting_uri': '" 230 + getUri(BUYER_1.toString(), BUYER_REPORTING_PATH).toString() 231 + "' } };\n" 232 + "}"; 233 234 // TODO(b/274837158) Uncomment after API is un-hidden 235 // String biddingLogicJsBuyer2 = 236 // "function generateBid(ad, auction_signals, per_buyer_signals," 237 // + " trusted_bidding_signals, contextual_signals," 238 // + " custom_audience_signals) { \n" 239 // + " return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };\n" 240 // + "}\n" 241 // + "function reportWin(ad_selection_signals, per_buyer_signals," 242 // + " signals_for_buyer, contextual_signals, custom_audience_signals) { \n" 243 // + " registerAdBeacon('click', '" 244 // + BUYER_2_CLICK_URI 245 // + "');\n" 246 // + " registerAdBeacon('hover', '" 247 // + BUYER_2_HOVER_URI 248 // + "');\n" 249 // + " return {'status': 0, 'results': {'reporting_uri': '" 250 // + getUri(BUYER_2.toString(), BUYER_REPORTING_PATH).toString() 251 // + "' } };\n" 252 // + "}"; 253 254 String biddingLogicJsBuyer2 = 255 "function generateBid(ad, auction_signals, per_buyer_signals," 256 + " trusted_bidding_signals, contextual_signals," 257 + " custom_audience_signals) { \n" 258 + " return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };\n" 259 + "}\n" 260 + "function reportWin(ad_selection_signals, per_buyer_signals," 261 + " signals_for_buyer, contextual_signals, custom_audience_signals) { \n" 262 + " return {'status': 0, 'results': {'reporting_uri': '" 263 + getUri(BUYER_2.toString(), BUYER_REPORTING_PATH).toString() 264 + "' } };\n" 265 + "}"; 266 267 List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2); 268 List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0); 269 270 CustomAudience customAudience1 = createCustomAudience(BUYER_1, bidsForBuyer1); 271 272 CustomAudience customAudience2 = createCustomAudience(BUYER_2, bidsForBuyer2); 273 274 try { 275 mCustomAudienceClient.joinCustomAudience(customAudience1).get(10, TimeUnit.SECONDS); 276 mCustomAudienceClient.joinCustomAudience(customAudience2).get(10, TimeUnit.SECONDS); 277 } catch (Exception e) { 278 String errorMessage = 279 String.format("Error setting up the test: message is %s", e.getMessage()); 280 Log.e(TAG, errorMessage); 281 throw new LoadSdkException(e, new Bundle()); 282 } 283 284 try { 285 AddAdSelectionOverrideRequest addAdSelectionOverrideRequest = 286 new AddAdSelectionOverrideRequest( 287 AD_SELECTION_CONFIG, decisionLogicJs, TRUSTED_SCORING_SIGNALS); 288 mTestAdSelectionClient 289 .overrideAdSelectionConfigRemoteInfo(addAdSelectionOverrideRequest) 290 .get(10, TimeUnit.SECONDS); 291 } catch (Exception e) { 292 String errorMessage = 293 String.format( 294 "Error adding ad selection override: message is %s", e.getMessage()); 295 Log.e(TAG, errorMessage); 296 throw new LoadSdkException(e, new Bundle()); 297 } 298 299 try { 300 AddCustomAudienceOverrideRequest addCustomAudienceOverrideRequest1 = 301 new AddCustomAudienceOverrideRequest.Builder() 302 .setBuyer(customAudience1.getBuyer()) 303 .setName(customAudience1.getName()) 304 .setBiddingLogicJs(biddingLogicJsBuyer1) 305 .setTrustedBiddingSignals(TRUSTED_BIDDING_SIGNALS) 306 .build(); 307 AddCustomAudienceOverrideRequest addCustomAudienceOverrideRequest2 = 308 new AddCustomAudienceOverrideRequest.Builder() 309 .setBuyer(customAudience2.getBuyer()) 310 .setName(customAudience2.getName()) 311 .setBiddingLogicJs(biddingLogicJsBuyer2) 312 .setTrustedBiddingSignals(TRUSTED_BIDDING_SIGNALS) 313 .build(); 314 315 mTestCustomAudienceClient 316 .overrideCustomAudienceRemoteInfo(addCustomAudienceOverrideRequest1) 317 .get(10, TimeUnit.SECONDS); 318 mTestCustomAudienceClient 319 .overrideCustomAudienceRemoteInfo(addCustomAudienceOverrideRequest2) 320 .get(10, TimeUnit.SECONDS); 321 } catch (Exception e) { 322 String errorMessage = 323 String.format( 324 "Error adding custom audience override: message is %s", e.getMessage()); 325 Log.e(TAG, errorMessage); 326 throw new LoadSdkException(e, new Bundle()); 327 } 328 329 Log.i( 330 TAG, 331 "Running ad selection with logic URI " + AD_SELECTION_CONFIG.getDecisionLogicUri()); 332 Log.i( 333 TAG, 334 "Decision logic URI domain is " 335 + AD_SELECTION_CONFIG.getDecisionLogicUri().getHost()); 336 337 long adSelectionId = -1; 338 try { 339 // Running ad selection and asserting that the outcome is returned in < 10 seconds 340 AdSelectionOutcome outcome = 341 mAdSelectionClient.selectAds(AD_SELECTION_CONFIG).get(10, TimeUnit.SECONDS); 342 343 adSelectionId = outcome.getAdSelectionId(); 344 345 if (!outcome.getRenderUri() 346 .equals(getUri(BUYER_2.toString(), AD_URI_PREFIX + "/ad3"))) { 347 String errorMessage = 348 String.format( 349 "Ad selection failed to select the correct ad, got %s instead", 350 outcome.getRenderUri().toString()); 351 Log.e(TAG, errorMessage); 352 throw new LoadSdkException(new Exception(errorMessage), new Bundle()); 353 } 354 } catch (Exception e) { 355 String errorMessage = 356 String.format( 357 "Error encountered during ad selection: message is %s", e.getMessage()); 358 Log.e(TAG, errorMessage); 359 throw new LoadSdkException(e, new Bundle()); 360 } 361 362 try { 363 ReportImpressionRequest reportImpressionRequest = 364 new ReportImpressionRequest(adSelectionId, AD_SELECTION_CONFIG); 365 366 // Performing reporting, and asserting that no exception is thrown 367 mAdSelectionClient.reportImpression(reportImpressionRequest).get(10, TimeUnit.SECONDS); 368 } catch (Exception e) { 369 String errorMessage = 370 String.format( 371 "Error encountered during reporting: message is %s", e.getMessage()); 372 Log.e(TAG, errorMessage); 373 throw new LoadSdkException(e, new Bundle()); 374 } 375 376 // TODO(b/274837158) Uncomment after API is un-hidden 377 378 // try { 379 // ReportEventRequest reportInteractionClickRequest = 380 // new ReportEventRequest( 381 // adSelectionId, 382 // CLICK_INTERACTION, 383 // INTERACTION_DATA, 384 // BUYER_DESTINATION | SELLER_DESTINATION); 385 // 386 // ReportEventRequest reportInteractionHoverRequest = 387 // new ReportEventRequest( 388 // adSelectionId, 389 // HOVER_INTERACTION, 390 // INTERACTION_DATA, 391 // BUYER_DESTINATION | SELLER_DESTINATION); 392 // 393 // // Performing interaction reporting, and asserting that no exception is thrown 394 // mAdSelectionClient 395 // .reportInteraction(reportInteractionClickRequest) 396 // .get(10, TimeUnit.SECONDS); 397 // mAdSelectionClient 398 // .reportInteraction(reportInteractionHoverRequest) 399 // .get(10, TimeUnit.SECONDS); 400 // } catch (Exception e) { 401 // String errorMessage = 402 // String.format( 403 // "Error encountered during interaction reporting: message is 404 // %s", e.getMessage()); 405 // Log.e(TAG, errorMessage); 406 // throw new LoadSdkException(e, new Bundle()); 407 // } 408 409 try { 410 UpdateAdCounterHistogramRequest updateHistogramRequest = 411 new UpdateAdCounterHistogramRequest.Builder( 412 adSelectionId, 413 FrequencyCapFilters.AD_EVENT_TYPE_CLICK, 414 AD_SELECTION_CONFIG.getSeller()) 415 .build(); 416 mAdSelectionClient 417 .updateAdCounterHistogram(updateHistogramRequest) 418 .get(10, TimeUnit.SECONDS); 419 } catch (Exception exception) { 420 String errorMessage = 421 String.format( 422 "Error encountered during ad counter histogram update: message is %s", 423 exception.getMessage()); 424 Log.e(TAG, errorMessage); 425 throw new LoadSdkException(exception, new Bundle()); 426 } 427 428 // If we got this far, that means the test succeeded 429 return new SandboxedSdk(new Binder()); 430 } 431 432 @Override getView( @onNull Context windowContext, @NonNull Bundle params, int width, int height)433 public View getView( 434 @NonNull Context windowContext, @NonNull Bundle params, int width, int height) { 435 return null; 436 } 437 setup()438 private void setup() { 439 mAdSelectionClient = 440 new AdSelectionClient.Builder() 441 .setContext(getContext()) 442 .setExecutor(CALLBACK_EXECUTOR) 443 .build(); 444 mTestAdSelectionClient = 445 new TestAdSelectionClient.Builder() 446 .setContext(getContext()) 447 .setExecutor(CALLBACK_EXECUTOR) 448 .build(); 449 mCustomAudienceClient = 450 new AdvertisingCustomAudienceClient.Builder() 451 .setContext(getContext()) 452 .setExecutor(MoreExecutors.directExecutor()) 453 .build(); 454 mTestCustomAudienceClient = 455 new TestAdvertisingCustomAudienceClient.Builder() 456 .setContext(getContext()) 457 .setExecutor(MoreExecutors.directExecutor()) 458 .build(); 459 } 460 461 /** 462 * @param buyer The name of the buyer for this Custom Audience 463 * @param bids these bids, are added to its metadata. Our JS logic then picks this value and 464 * creates ad with the provided value as bid 465 * @return a real Custom Audience object that can be persisted and used in bidding and scoring 466 */ createCustomAudience(final AdTechIdentifier buyer, List<Double> bids)467 private CustomAudience createCustomAudience(final AdTechIdentifier buyer, List<Double> bids) { 468 469 // Generate ads for with bids provided 470 List<AdData> ads = new ArrayList<>(); 471 472 // Create ads with the buyer name and bid number as the ad URI 473 // Add the bid value to the metadata 474 for (int i = 0; i < bids.size(); i++) { 475 ads.add( 476 new AdData.Builder() 477 .setRenderUri(getUri(buyer.toString(), AD_URI_PREFIX + "/ad" + (i + 1))) 478 .setMetadata("{\"result\":" + bids.get(i) + "}") 479 .build()); 480 } 481 482 return new CustomAudience.Builder() 483 .setBuyer(buyer) 484 .setName(buyer + "testCustomAudienceName") 485 .setActivationTime(Instant.now().truncatedTo(ChronoUnit.MILLIS)) 486 .setExpirationTime(Instant.now().plus(Duration.ofDays(40))) 487 .setDailyUpdateUri(getUri(buyer.toString(), "/update")) 488 .setUserBiddingSignals( 489 AdSelectionSignals.fromString("{'valid': 'yep', 'opaque': 'definitely'}")) 490 .setTrustedBiddingData( 491 new TrustedBiddingData.Builder() 492 .setTrustedBiddingKeys( 493 Arrays.asList("example", "valid", "list", "of", "keys")) 494 .setTrustedBiddingUri(getUri(buyer.toString(), "/trusted/bidding")) 495 .build()) 496 .setBiddingLogicUri(getUri(buyer.toString(), BUYER_BIDDING_LOGIC_URI_PATH)) 497 .setAds(ads) 498 .build(); 499 } 500 anAdSelectionConfigBuilder()501 public static AdSelectionConfig.Builder anAdSelectionConfigBuilder() { 502 return new AdSelectionConfig.Builder() 503 .setSeller(SELLER) 504 .setDecisionLogicUri(getUri(SELLER.toString(), "/update")) 505 .setCustomAudienceBuyers(Arrays.asList(BUYER_1, BUYER_2)) 506 .setAdSelectionSignals(AdSelectionSignals.EMPTY) 507 .setSellerSignals(AdSelectionSignals.fromString("{\"test_seller_signals\":1}")) 508 .setPerBuyerSignals( 509 Map.of( 510 BUYER_1, 511 AdSelectionSignals.fromString("{\"buyer_signals\":1}"), 512 BUYER_2, 513 AdSelectionSignals.fromString("{\"buyer_signals\":2}"))) 514 .setTrustedScoringSignalsUri(getUri(SELLER.toString(), "/trusted/scoring")); 515 } 516 getUri(String host, String path)517 private static Uri getUri(String host, String path) { 518 return Uri.parse(HTTPS_SCHEME + "://" + host + path); 519 } 520 } 521