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