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