• 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 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