1 /* 2 * Copyright (C) 2023 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 static android.adservices.adselection.ReportEventRequest.FLAG_REPORTING_DESTINATION_BUYER; 20 import static android.adservices.adselection.ReportEventRequest.FLAG_REPORTING_DESTINATION_SELLER; 21 import static android.adservices.adselection.ReportEventRequest.REPORT_EVENT_MAX_INTERACTION_DATA_SIZE_B; 22 23 import static com.android.adservices.service.common.FledgeAuthorizationFilter.AdTechNotAllowedException; 24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__REPORT_INTERACTION; 25 26 import static java.util.Locale.ENGLISH; 27 28 import android.adservices.adselection.ReportEventRequest; 29 import android.adservices.adselection.ReportInteractionCallback; 30 import android.adservices.adselection.ReportInteractionInput; 31 import android.adservices.common.AdServicesStatusUtils; 32 import android.adservices.common.AdTechIdentifier; 33 import android.adservices.common.FledgeErrorResponse; 34 import android.annotation.NonNull; 35 import android.net.Uri; 36 import android.os.Build; 37 import android.os.RemoteException; 38 import android.os.Trace; 39 40 import androidx.annotation.RequiresApi; 41 42 import com.android.adservices.LoggerFactory; 43 import com.android.adservices.data.adselection.AdSelectionEntryDao; 44 import com.android.adservices.service.Flags; 45 import com.android.adservices.service.common.AdSelectionServiceFilter; 46 import com.android.adservices.service.common.FledgeAuthorizationFilter; 47 import com.android.adservices.service.common.Throttler; 48 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient; 49 import com.android.adservices.service.devapi.DevContext; 50 import com.android.adservices.service.exception.FilterException; 51 import com.android.adservices.service.profiling.Tracing; 52 import com.android.adservices.service.stats.AdServicesLogger; 53 import com.android.internal.util.Preconditions; 54 55 import com.google.common.util.concurrent.FluentFuture; 56 import com.google.common.util.concurrent.Futures; 57 import com.google.common.util.concurrent.ListenableFuture; 58 import com.google.common.util.concurrent.ListeningExecutorService; 59 import com.google.common.util.concurrent.MoreExecutors; 60 61 import java.nio.charset.StandardCharsets; 62 import java.util.ArrayList; 63 import java.util.List; 64 import java.util.Objects; 65 import java.util.concurrent.ExecutorService; 66 67 /** Encapsulates the Event Reporting logic */ 68 @RequiresApi(Build.VERSION_CODES.S) 69 public abstract class EventReporter { 70 public static final String NO_MATCH_FOUND_IN_AD_SELECTION_DB = 71 "Could not find a match in the database for this adSelectionId and callerPackageName!"; 72 public static final String INTERACTION_DATA_SIZE_MAX_EXCEEDED = "Event data max size exceeded!"; 73 public static final String INTERACTION_KEY_SIZE_MAX_EXCEEDED = "Event key max size exceeded!"; 74 static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 75 static final int LOGGING_API_NAME = AD_SERVICES_API_CALLED__API_NAME__REPORT_INTERACTION; 76 77 @ReportEventRequest.ReportingDestination 78 private static final int[] POSSIBLE_DESTINATIONS = 79 new int[] {FLAG_REPORTING_DESTINATION_SELLER, FLAG_REPORTING_DESTINATION_BUYER}; 80 81 @NonNull private final AdSelectionEntryDao mAdSelectionEntryDao; 82 @NonNull private final AdServicesHttpsClient mAdServicesHttpsClient; 83 @NonNull final ListeningExecutorService mLightweightExecutorService; 84 @NonNull private final ListeningExecutorService mBackgroundExecutorService; 85 @NonNull final AdServicesLogger mAdServicesLogger; 86 @NonNull final Flags mFlags; 87 @NonNull private final AdSelectionServiceFilter mAdSelectionServiceFilter; 88 private int mCallerUid; 89 @NonNull private final FledgeAuthorizationFilter mFledgeAuthorizationFilter; 90 @NonNull private final DevContext mDevContext; 91 private final boolean mShouldUseUnifiedTables; 92 EventReporter( @onNull AdSelectionEntryDao adSelectionEntryDao, @NonNull AdServicesHttpsClient adServicesHttpsClient, @NonNull ExecutorService lightweightExecutorService, @NonNull ExecutorService backgroundExecutorService, @NonNull AdServicesLogger adServicesLogger, @NonNull Flags flags, @NonNull AdSelectionServiceFilter adSelectionServiceFilter, int callerUid, @NonNull FledgeAuthorizationFilter fledgeAuthorizationFilter, @NonNull DevContext devContext, boolean shouldUseUnifiedTables)93 public EventReporter( 94 @NonNull AdSelectionEntryDao adSelectionEntryDao, 95 @NonNull AdServicesHttpsClient adServicesHttpsClient, 96 @NonNull ExecutorService lightweightExecutorService, 97 @NonNull ExecutorService backgroundExecutorService, 98 @NonNull AdServicesLogger adServicesLogger, 99 @NonNull Flags flags, 100 @NonNull AdSelectionServiceFilter adSelectionServiceFilter, 101 int callerUid, 102 @NonNull FledgeAuthorizationFilter fledgeAuthorizationFilter, 103 @NonNull DevContext devContext, 104 boolean shouldUseUnifiedTables) { 105 Objects.requireNonNull(adSelectionEntryDao); 106 Objects.requireNonNull(adServicesHttpsClient); 107 Objects.requireNonNull(lightweightExecutorService); 108 Objects.requireNonNull(backgroundExecutorService); 109 Objects.requireNonNull(adServicesLogger); 110 Objects.requireNonNull(flags); 111 Objects.requireNonNull(adSelectionServiceFilter); 112 Objects.requireNonNull(fledgeAuthorizationFilter); 113 Objects.requireNonNull(devContext); 114 115 mAdSelectionEntryDao = adSelectionEntryDao; 116 mAdServicesHttpsClient = adServicesHttpsClient; 117 mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService); 118 mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService); 119 mAdServicesLogger = adServicesLogger; 120 mFlags = flags; 121 mAdSelectionServiceFilter = adSelectionServiceFilter; 122 mCallerUid = callerUid; 123 mFledgeAuthorizationFilter = fledgeAuthorizationFilter; 124 mDevContext = devContext; 125 mShouldUseUnifiedTables = shouldUseUnifiedTables; 126 } 127 128 /** 129 * Run the interaction report logic asynchronously. Searches the {@code 130 * registered_ad_interactions} database for matches based on the provided {@code adSelectionId}, 131 * {@code interactionKey}, {@code destinations} that we get from {@link ReportInteractionInput} 132 * Then, attaches {@code interactionData} to each found Uri and performs a POST request. 133 * 134 * <p>After validating the inputParams and request context, invokes {@link 135 * ReportInteractionCallback#onSuccess()} before continuing with reporting. If we encounter a 136 * failure during request validation, we invoke {@link 137 * ReportInteractionCallback#onFailure(FledgeErrorResponse)} and exit early. 138 */ reportInteraction( @onNull ReportInteractionInput input, @NonNull ReportInteractionCallback callback)139 public abstract void reportInteraction( 140 @NonNull ReportInteractionInput input, @NonNull ReportInteractionCallback callback); 141 filterAndValidateRequest(ReportInteractionInput input)142 void filterAndValidateRequest(ReportInteractionInput input) { 143 long registeredAdBeaconsMaxInteractionKeySizeB = 144 mFlags.getFledgeReportImpressionRegisteredAdBeaconsMaxInteractionKeySizeB(); 145 146 try { 147 Trace.beginSection(Tracing.VALIDATE_REQUEST); 148 sLogger.v("Starting filtering and validation."); 149 mAdSelectionServiceFilter.filterRequest( 150 null, 151 input.getCallerPackageName(), 152 mFlags.getEnforceForegroundStatusForFledgeReportInteraction(), 153 true, 154 mCallerUid, 155 LOGGING_API_NAME, 156 Throttler.ApiKey.FLEDGE_API_REPORT_INTERACTION, 157 mDevContext); 158 validateAdSelectionIdAndCallerPackageNameExistence( 159 input.getAdSelectionId(), input.getCallerPackageName()); 160 Preconditions.checkArgument( 161 input.getInteractionKey().getBytes(StandardCharsets.UTF_8).length 162 <= registeredAdBeaconsMaxInteractionKeySizeB, 163 INTERACTION_KEY_SIZE_MAX_EXCEEDED); 164 Preconditions.checkArgument( 165 input.getInteractionData().getBytes(StandardCharsets.UTF_8).length 166 <= REPORT_EVENT_MAX_INTERACTION_DATA_SIZE_B, 167 INTERACTION_DATA_SIZE_MAX_EXCEEDED); 168 } finally { 169 sLogger.v("Completed filtering and validation."); 170 Trace.endSection(); 171 } 172 } 173 getReportingUris(ReportInteractionInput input)174 FluentFuture<List<Uri>> getReportingUris(ReportInteractionInput input) { 175 sLogger.v( 176 "Fetching ad selection entry ID %d for caller \"%s\"", 177 input.getAdSelectionId(), input.getCallerPackageName()); 178 long adSelectionId = input.getAdSelectionId(); 179 int destinationsBitField = input.getReportingDestinations(); 180 String interactionKey = input.getInteractionKey(); 181 182 return FluentFuture.from( 183 mBackgroundExecutorService.submit( 184 () -> { 185 List<Uri> resultingReportingUris = new ArrayList<>(); 186 for (int destination : POSSIBLE_DESTINATIONS) { 187 if (bitExists(destination, destinationsBitField)) { 188 if (mAdSelectionEntryDao 189 .doesRegisteredAdInteractionExist( 190 adSelectionId, 191 interactionKey, 192 destination)) { 193 sLogger.v( 194 "Found registered ad beacons for" 195 + " id:%s, key:%s and dest:%s", 196 adSelectionId, interactionKey, destination); 197 resultingReportingUris.add( 198 mAdSelectionEntryDao 199 .getRegisteredAdInteractionUri( 200 adSelectionId, 201 interactionKey, 202 destination)); 203 } else { 204 sLogger.w( 205 "Registered ad beacon URIs not found for" 206 + " id:%s, key:%s and dest:%s", 207 adSelectionId, interactionKey, destination); 208 } 209 } 210 } 211 return resultingReportingUris; 212 })) 213 .transformAsync(this::filterReportingUris, mLightweightExecutorService); 214 } 215 filterReportingUris(List<Uri> reportingUris)216 private FluentFuture<List<Uri>> filterReportingUris(List<Uri> reportingUris) { 217 return FluentFuture.from( 218 mLightweightExecutorService.submit( 219 () -> { 220 if (mFlags.getDisableFledgeEnrollmentCheck()) { 221 return reportingUris; 222 } else { 223 // Do enrollment check and only add Uris that pass enrollment 224 ArrayList<Uri> validatedUris = new ArrayList<>(); 225 226 for (Uri uri : reportingUris) { 227 try { 228 mFledgeAuthorizationFilter.assertAdTechEnrolled( 229 AdTechIdentifier.fromString(uri.getHost()), 230 LOGGING_API_NAME); 231 validatedUris.add(uri); 232 } catch (AdTechNotAllowedException exception) { 233 sLogger.d( 234 String.format( 235 ENGLISH, 236 "Enrollment check failed! Skipping" 237 + " reporting for %s:", 238 uri)); 239 } 240 } 241 sLogger.v("Validated uris: %s", validatedUris); 242 return validatedUris; 243 } 244 })); 245 } 246 247 ListenableFuture<List<Void>> reportUris(List<Uri> reportingUris, ReportInteractionInput input) { 248 List<ListenableFuture<Void>> reportingFuturesList = new ArrayList<>(); 249 String eventData = input.getInteractionData(); 250 251 for (Uri uri : reportingUris) { 252 sLogger.v("Uri to report the event: %s.", uri); 253 reportingFuturesList.add( 254 mAdServicesHttpsClient.postPlainText(uri, eventData, mDevContext)); 255 } 256 return Futures.allAsList(reportingFuturesList); 257 } 258 259 void notifySuccessToCaller(@NonNull ReportInteractionCallback callback) { 260 try { 261 callback.onSuccess(); 262 } catch (RemoteException e) { 263 sLogger.e(e, "Unable to send successful result to the callback"); 264 } 265 } 266 267 void notifyFailureToCaller( 268 @NonNull String callerAppPackageName, 269 @NonNull ReportInteractionCallback callback, 270 @NonNull Throwable t) { 271 int resultCode; 272 273 boolean isFilterException = t instanceof FilterException; 274 275 if (isFilterException) { 276 resultCode = FilterException.getResultCode(t); 277 } else if (t instanceof IllegalArgumentException) { 278 resultCode = AdServicesStatusUtils.STATUS_INVALID_ARGUMENT; 279 } else { 280 resultCode = AdServicesStatusUtils.STATUS_INTERNAL_ERROR; 281 } 282 283 // Skip logging if a FilterException occurs. 284 // AdSelectionServiceFilter ensures the failing assertion is logged internally. 285 // Note: Failure is logged before the callback to ensure deterministic testing. 286 if (!isFilterException) { 287 mAdServicesLogger.logFledgeApiCallStats( 288 LOGGING_API_NAME, callerAppPackageName, resultCode, /*latencyMs=*/ 0); 289 } 290 291 try { 292 callback.onFailure( 293 new FledgeErrorResponse.Builder() 294 .setStatusCode(resultCode) 295 .setErrorMessage(t.getMessage()) 296 .build()); 297 } catch (RemoteException e) { 298 sLogger.e(e, "Unable to send failed result to the callback"); 299 } 300 } 301 302 private void validateAdSelectionIdAndCallerPackageNameExistence( 303 long adSelectionId, String callerPackageName) { 304 if (mFlags.getFledgeAuctionServerEnabledForReportEvent() || mShouldUseUnifiedTables) { 305 Preconditions.checkArgument( 306 mAdSelectionEntryDao.doesAdSelectionIdAndCallerPackageNameExists( 307 adSelectionId, callerPackageName), 308 NO_MATCH_FOUND_IN_AD_SELECTION_DB); 309 310 } else { 311 Preconditions.checkArgument( 312 mAdSelectionEntryDao 313 .doesAdSelectionMatchingCallerPackageNameExistInOnDeviceTable( 314 adSelectionId, callerPackageName), 315 NO_MATCH_FOUND_IN_AD_SELECTION_DB); 316 } 317 } 318 319 private boolean bitExists( 320 @ReportEventRequest.ReportingDestination int bit, 321 @ReportEventRequest.ReportingDestination int bitSet) { 322 return (bit & bitSet) != 0; 323 } 324 } 325