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