• 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.adservices.service.adselection;
18 
19 import android.adservices.adselection.AdSelectionConfig;
20 import android.adservices.common.AdSelectionSignals;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.os.Build;
26 import android.os.Process;
27 import android.util.Pair;
28 
29 import androidx.annotation.RequiresApi;
30 
31 import com.android.adservices.LoggerFactory;
32 import com.android.adservices.data.adselection.AdSelectionEntryDao;
33 import com.android.adservices.data.adselection.CustomAudienceSignals;
34 import com.android.adservices.data.adselection.DBAdSelection;
35 import com.android.adservices.data.customaudience.CustomAudienceDao;
36 import com.android.adservices.data.customaudience.DBCustomAudience;
37 import com.android.adservices.service.Flags;
38 import com.android.adservices.service.common.AdSelectionServiceFilter;
39 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
40 import com.android.adservices.service.devapi.CustomAudienceDevOverridesHelper;
41 import com.android.adservices.service.devapi.DevContext;
42 import com.android.adservices.service.devapi.DevContextFilter;
43 import com.android.adservices.service.proto.SellerFrontEndGrpc;
44 import com.android.adservices.service.proto.SellerFrontendService.BuyerInput;
45 import com.android.adservices.service.proto.SellerFrontendService.SelectWinningAdRequest;
46 import com.android.adservices.service.proto.SellerFrontendService.SelectWinningAdRequest.SelectWinningAdRawRequest.ClientType;
47 import com.android.adservices.service.proto.SellerFrontendService.SelectWinningAdResponse;
48 import com.android.adservices.service.stats.AdSelectionExecutionLogger;
49 import com.android.adservices.service.stats.AdServicesLogger;
50 import com.android.internal.annotations.VisibleForTesting;
51 
52 import com.google.common.base.Function;
53 import com.google.common.collect.ImmutableList;
54 import com.google.common.collect.Iterables;
55 import com.google.common.util.concurrent.AsyncFunction;
56 import com.google.common.util.concurrent.FluentFuture;
57 import com.google.common.util.concurrent.ListenableFuture;
58 import com.google.common.util.concurrent.UncheckedTimeoutException;
59 import com.google.protobuf.Struct;
60 import com.google.protobuf.Value;
61 
62 import org.json.JSONException;
63 import org.json.JSONObject;
64 
65 import java.time.Clock;
66 import java.util.HashMap;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.NoSuchElementException;
70 import java.util.concurrent.ExecutionException;
71 import java.util.concurrent.ExecutorService;
72 import java.util.concurrent.ScheduledThreadPoolExecutor;
73 import java.util.concurrent.TimeUnit;
74 import java.util.concurrent.TimeoutException;
75 import java.util.stream.Collectors;
76 
77 import io.grpc.Codec;
78 import io.grpc.ManagedChannel;
79 import io.grpc.okhttp.OkHttpChannelBuilder;
80 
81 /**
82  * Offload execution to Bidding & Auction services. Sends an umbrella request to the Seller Frontend
83  * Service.
84  */
85 // TODO(b/269798827): Enable for R.
86 @RequiresApi(Build.VERSION_CODES.S)
87 public class TrustedServerAdSelectionRunner extends AdSelectionRunner {
88     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
89     public static final String GZIP = new Codec.Gzip().getMessageEncoding(); // "gzip"
90     @NonNull private final CustomAudienceDevOverridesHelper mCustomAudienceDevOverridesHelper;
91     @NonNull private final JsFetcher mJsFetcher;
92 
TrustedServerAdSelectionRunner( @onNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, @NonNull final AdServicesHttpsClient adServicesHttpsClient, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @NonNull final AdServicesLogger adServicesLogger, @NonNull final DevContext devContext, @NonNull final Flags flags, @NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger, @NonNull final AdSelectionServiceFilter adSelectionServiceFilter, @NonNull final AdFilterer adFilterer, int callerUid)93     public TrustedServerAdSelectionRunner(
94             @NonNull final Context context,
95             @NonNull final CustomAudienceDao customAudienceDao,
96             @NonNull final AdSelectionEntryDao adSelectionEntryDao,
97             @NonNull final AdServicesHttpsClient adServicesHttpsClient,
98             @NonNull final ExecutorService lightweightExecutorService,
99             @NonNull final ExecutorService backgroundExecutorService,
100             @NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
101             @NonNull final AdServicesLogger adServicesLogger,
102             @NonNull final DevContext devContext,
103             @NonNull final Flags flags,
104             @NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger,
105             @NonNull final AdSelectionServiceFilter adSelectionServiceFilter,
106             @NonNull final AdFilterer adFilterer,
107             int callerUid) {
108         super(
109                 context,
110                 customAudienceDao,
111                 adSelectionEntryDao,
112                 lightweightExecutorService,
113                 backgroundExecutorService,
114                 scheduledExecutor,
115                 adServicesLogger,
116                 flags,
117                 adSelectionExecutionLogger,
118                 adSelectionServiceFilter,
119                 adFilterer,
120                 callerUid);
121 
122         mCustomAudienceDevOverridesHelper =
123                 new CustomAudienceDevOverridesHelper(devContext, customAudienceDao);
124         mJsFetcher =
125                 new JsFetcher(
126                         mBackgroundExecutorService,
127                         mLightweightExecutorService,
128                         adServicesHttpsClient,
129                         flags);
130     }
131 
132     @VisibleForTesting
TrustedServerAdSelectionRunner( @onNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @NonNull final AdSelectionIdGenerator adSelectionIdGenerator, @NonNull Clock clock, @NonNull final AdServicesLogger adServicesLogger, @NonNull final Flags flags, int callerUid, @NonNull final AdSelectionServiceFilter adSelectionServiceFilter, @NonNull final AdFilterer adFilterer, @NonNull final JsFetcher jsFetcher, @NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger)133     TrustedServerAdSelectionRunner(
134             @NonNull final Context context,
135             @NonNull final CustomAudienceDao customAudienceDao,
136             @NonNull final AdSelectionEntryDao adSelectionEntryDao,
137             @NonNull final ExecutorService lightweightExecutorService,
138             @NonNull final ExecutorService backgroundExecutorService,
139             @NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
140             @NonNull final AdSelectionIdGenerator adSelectionIdGenerator,
141             @NonNull Clock clock,
142             @NonNull final AdServicesLogger adServicesLogger,
143             @NonNull final Flags flags,
144             int callerUid,
145             @NonNull final AdSelectionServiceFilter adSelectionServiceFilter,
146             @NonNull final AdFilterer adFilterer,
147             @NonNull final JsFetcher jsFetcher,
148             @NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger) {
149         super(
150                 context,
151                 customAudienceDao,
152                 adSelectionEntryDao,
153                 lightweightExecutorService,
154                 backgroundExecutorService,
155                 scheduledExecutor,
156                 adSelectionIdGenerator,
157                 clock,
158                 adServicesLogger,
159                 flags,
160                 callerUid,
161                 adSelectionServiceFilter,
162                 adFilterer,
163                 adSelectionExecutionLogger);
164 
165         this.mJsFetcher = jsFetcher;
166         DevContext devContext = DevContextFilter.create(context).createDevContext(Process.myUid());
167         this.mCustomAudienceDevOverridesHelper =
168                 new CustomAudienceDevOverridesHelper(devContext, customAudienceDao);
169     }
170 
171     /** Prepares request and calls Seller Front-end Service to orchestrate ad selection. */
orchestrateAdSelection( @onNull final AdSelectionConfig adSelectionConfig, @NonNull final String callerPackageName, @NonNull ListenableFuture<List<DBCustomAudience>> buyersCustomAudiences)172     public ListenableFuture<AdSelectionOrchestrationResult> orchestrateAdSelection(
173             @NonNull final AdSelectionConfig adSelectionConfig,
174             @NonNull final String callerPackageName,
175             @NonNull ListenableFuture<List<DBCustomAudience>> buyersCustomAudiences) {
176 
177         Function<List<DBCustomAudience>, Map<String, BuyerInput>> createBuyerInputs =
178                 buyerCAs -> {
179                     return createBuyerInputs(buyerCAs, adSelectionConfig);
180                 };
181 
182         Function<Map<String, BuyerInput>, SelectWinningAdRequest> createSelectWinningAdRequest =
183                 encryptedInputPerBuyer -> {
184                     return createSelectWinningAdRequest(adSelectionConfig, encryptedInputPerBuyer);
185                 };
186 
187         AsyncFunction<SelectWinningAdRequest, SelectWinningAdResponse> callSelectWinningAd =
188                 req -> {
189                     return callSelectWinningAd(req);
190                 };
191 
192         // Return the DBCustomAudience to fetch the buyerLogicJs in the next future.
193         Function<SelectWinningAdResponse, Pair<DBAdSelection.Builder, DBCustomAudience>>
194                 getCustomAudienceAndDBAdSelection =
195                         selectWinningAdResponse -> {
196                             return getCustomAudienceAndDBAdSelection(
197                                     selectWinningAdResponse,
198                                     callerPackageName,
199                                     buyersCustomAudiences);
200                         };
201 
202         // TODO(b/254066067): Confirm if buyer logic for reporting can be fetched after rendering.
203         AsyncFunction<
204                         Pair<DBAdSelection.Builder, DBCustomAudience>,
205                         Pair<DBAdSelection.Builder, FluentFuture<String>>>
206                 fetchBuyerLogicJs =
207                         dbAdSelectionAndCAPair -> {
208                             return fetchBuyerLogicJs(dbAdSelectionAndCAPair);
209                         };
210 
211         Function<Pair<DBAdSelection.Builder, FluentFuture<String>>, AdSelectionOrchestrationResult>
212                 createAdSelectionResult =
213                         dbAdSelectionAndBuyerLogicJsPair -> {
214                             return createAdSelectionResult(dbAdSelectionAndBuyerLogicJsPair);
215                         };
216 
217         return FluentFuture.from(buyersCustomAudiences)
218                 .transform(createBuyerInputs, mLightweightExecutorService)
219                 .transform(createSelectWinningAdRequest, mLightweightExecutorService)
220                 .transformAsync(callSelectWinningAd, mBackgroundExecutorService)
221                 .transform(getCustomAudienceAndDBAdSelection, mLightweightExecutorService)
222                 .transformAsync(fetchBuyerLogicJs, mBackgroundExecutorService)
223                 .transform(createAdSelectionResult, mLightweightExecutorService)
224                 .withTimeout(
225                         mFlags.getAdSelectionOffDeviceOverallTimeoutMs(),
226                         TimeUnit.MILLISECONDS,
227                         mScheduledExecutor)
228                 .catching(
229                         TimeoutException.class,
230                         this::handleTimeoutError,
231                         mLightweightExecutorService);
232     }
233 
createBuyerInputs( List<DBCustomAudience> buyerCAs, AdSelectionConfig adSelectionConfig)234     private Map<String, BuyerInput> createBuyerInputs(
235             List<DBCustomAudience> buyerCAs, AdSelectionConfig adSelectionConfig) {
236         Map<String, BuyerInput> buyerInputs = new HashMap<>();
237         for (DBCustomAudience customAudience : buyerCAs) {
238             BuyerInput.CustomAudience.Builder customAudienceBuilder =
239                     BuyerInput.CustomAudience.newBuilder()
240                             .setName(customAudience.getName())
241                             .addAllBiddingSignalsKeys(getBiddingSignalKeys(customAudience));
242 
243             AdSelectionSignals perBuyerSignals =
244                     adSelectionConfig.getPerBuyerSignals().get(customAudience.getBuyer());
245             BuyerInput input =
246                     BuyerInput.newBuilder()
247                             .addCustomAudiences(customAudienceBuilder)
248                             .setBuyerSignals(convertSignalsToStruct(perBuyerSignals))
249                             .build();
250             // TODO(b/254325545): Update the key to the domain of the BFE service, not buyer name.
251             buyerInputs.put(customAudience.getBuyer().toString(), input);
252         }
253 
254         return buyerInputs;
255     }
256 
getBiddingSignalKeys(DBCustomAudience customAudience)257     private List<String> getBiddingSignalKeys(DBCustomAudience customAudience) {
258         List<String> biddingSignalKeys = customAudience.getTrustedBiddingData().getKeys();
259         // If the bidding signal keys is just the CA name, we don't need to pass it to the server.
260         if (biddingSignalKeys.size() == 1
261                 && customAudience.getName().equals(biddingSignalKeys.get(0))) {
262             return ImmutableList.of();
263         }
264 
265         // Remove the CA name from the bidding signal keys list to save space.
266         biddingSignalKeys.remove(customAudience.getName());
267         return biddingSignalKeys;
268     }
269 
createSelectWinningAdRequest( AdSelectionConfig adSelectionConfig, Map<String, BuyerInput> rawInputPerBuyer)270     private SelectWinningAdRequest createSelectWinningAdRequest(
271             AdSelectionConfig adSelectionConfig, Map<String, BuyerInput> rawInputPerBuyer) {
272         SelectWinningAdRequest.SelectWinningAdRawRequest.AuctionConfig.Builder auctionConfig =
273                 SelectWinningAdRequest.SelectWinningAdRawRequest.AuctionConfig.newBuilder()
274                         .setSellerSignals(
275                                 convertSignalsToStruct((adSelectionConfig.getSellerSignals())))
276                         // TODO(b/254068070): Check if this is contextually derived auction_signals.
277                         .setAuctionSignals(
278                                 convertSignalsToStruct(adSelectionConfig.getAdSelectionSignals()));
279 
280         SelectWinningAdRequest.SelectWinningAdRawRequest.Builder rawRequestBuilder =
281                 SelectWinningAdRequest.SelectWinningAdRawRequest.newBuilder()
282                         .setAdSelectionRequestId(mAdSelectionIdGenerator.generateId())
283                         .putAllRawBuyerInput(rawInputPerBuyer)
284                         .setAuctionConfig(auctionConfig)
285                         // FLEDGE is currently only supported on GMS core devices.
286                         .setClientType(ClientType.ANDROID);
287 
288         return SelectWinningAdRequest.newBuilder().setRawRequest(rawRequestBuilder).build();
289     }
290 
callSelectWinningAd( SelectWinningAdRequest req)291     private ListenableFuture<SelectWinningAdResponse> callSelectWinningAd(
292             SelectWinningAdRequest req) {
293         // TODO(b/249575366): Pass in address + port when the fields are added.
294         ManagedChannel channel = OkHttpChannelBuilder.forAddress("localhost", 8080).build();
295         SellerFrontEndGrpc.SellerFrontEndFutureStub stub =
296                 SellerFrontEndGrpc.newFutureStub(channel);
297 
298         if (mFlags.getAdSelectionOffDeviceRequestCompressionEnabled()) {
299             stub = stub.withCompression(GZIP);
300         }
301 
302         return stub.selectWinningAd(req);
303     }
304 
getCustomAudienceAndDBAdSelection( SelectWinningAdResponse selectWinningAdResponse, String callerPackageName, ListenableFuture<List<DBCustomAudience>> buyerCustomAudiences)305     private Pair<DBAdSelection.Builder, DBCustomAudience> getCustomAudienceAndDBAdSelection(
306             SelectWinningAdResponse selectWinningAdResponse,
307             String callerPackageName,
308             ListenableFuture<List<DBCustomAudience>> buyerCustomAudiences) {
309         SelectWinningAdResponse.SelectWinningAdRawResponse rawResponse =
310                 selectWinningAdResponse.getRawResponse();
311         Uri winningAdRenderUri = Uri.parse(rawResponse.getAdRenderUrl());
312 
313         // Find custom audience of the winning ad.
314         DBCustomAudience customAudience;
315         try {
316             // buyerCustomAudiences's future is already complete by the time this method is called.
317             List<DBCustomAudience> customAudiences = buyerCustomAudiences.get();
318             List<DBCustomAudience> filteredCustomAudiences =
319                     customAudiences.stream()
320                             .filter(
321                                     audience ->
322                                             audience.getName()
323                                                     .equals(rawResponse.getCustomAudienceName()))
324                             .collect(Collectors.toList());
325             customAudience = Iterables.getOnlyElement(filteredCustomAudiences);
326         } catch (InterruptedException | ExecutionException e) {
327             // Will never be thrown since the future has already completed for the code to be here.
328             throw new RuntimeException("Could not read buyerCustomAudiences list from device");
329         } catch (NoSuchElementException e) {
330             throw new IllegalStateException(
331                     "Could not find corresponding custom audience returned from Bidding & Auction"
332                             + " services");
333         }
334 
335         CustomAudienceSignals customAudienceSignals =
336                 CustomAudienceSignals.buildFromCustomAudience(customAudience);
337         DBAdSelection.Builder builder =
338                 new DBAdSelection.Builder()
339                         .setWinningAdBid(rawResponse.getBidPrice())
340                         .setWinningAdRenderUri(winningAdRenderUri)
341                         .setCustomAudienceSignals(customAudienceSignals)
342                         .setBiddingLogicUri(customAudience.getBiddingLogicUri())
343                         .setContextualSignals("{}")
344                         .setCallerPackageName(callerPackageName);
345 
346         return new Pair<>(builder, customAudience);
347     }
348 
fetchBuyerLogicJs( Pair<DBAdSelection.Builder, DBCustomAudience> dbAdSelectionAndCAPair)349     private ListenableFuture<Pair<DBAdSelection.Builder, FluentFuture<String>>> fetchBuyerLogicJs(
350             Pair<DBAdSelection.Builder, DBCustomAudience> dbAdSelectionAndCAPair) {
351         return mBackgroundExecutorService.submit(
352                 () -> {
353                     DBCustomAudience customAudience = dbAdSelectionAndCAPair.second;
354                     FluentFuture<String> buyerDecisionLogic =
355                             mJsFetcher.getBiddingLogic(
356                                     customAudience.getBiddingLogicUri(),
357                                     mCustomAudienceDevOverridesHelper,
358                                     customAudience.getOwner(),
359                                     customAudience.getBuyer(),
360                                     customAudience.getName());
361                     return new Pair<>(dbAdSelectionAndCAPair.first, buyerDecisionLogic);
362                 });
363     }
364 
createAdSelectionResult( Pair<DBAdSelection.Builder, FluentFuture<String>> dbAdSelectionAndBuyerLogicJsPair)365     private AdSelectionOrchestrationResult createAdSelectionResult(
366             Pair<DBAdSelection.Builder, FluentFuture<String>> dbAdSelectionAndBuyerLogicJsPair) {
367         try {
368             String buyerJsLogic = dbAdSelectionAndBuyerLogicJsPair.second.get();
369             return new AdSelectionOrchestrationResult(
370                     dbAdSelectionAndBuyerLogicJsPair.first, buyerJsLogic);
371         } catch (ExecutionException | InterruptedException e) {
372             throw new RuntimeException("Could not fetch buyerJsLogic", e);
373         }
374     }
375 
convertSignalsToStruct(AdSelectionSignals adSelectionSignals)376     private Struct convertSignalsToStruct(AdSelectionSignals adSelectionSignals) {
377         Struct.Builder signals = Struct.newBuilder();
378         try {
379             JSONObject json = new JSONObject(adSelectionSignals.toString());
380             for (String keyStr : json.keySet()) {
381                 Object obj = json.get(keyStr);
382                 if (obj instanceof String) {
383                     signals.putFields(
384                             keyStr, Value.newBuilder().setStringValue((String) obj).build());
385                 }
386             }
387         } catch (JSONException e) {
388             String error = "Invalid JSON found during SelectWinningAdRequest construction";
389             throw new IllegalArgumentException(error, e);
390         }
391 
392         return signals.build();
393     }
394 
395     @Nullable
handleTimeoutError(TimeoutException e)396     private AdSelectionOrchestrationResult handleTimeoutError(TimeoutException e) {
397         sLogger.e(e, "Ad Selection exceeded time limit");
398         throw new UncheckedTimeoutException(AD_SELECTION_TIMED_OUT);
399     }
400 }
401