• 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.measurement.reporting;
18 
19 import static com.android.adservices.service.measurement.reporting.ReportingStatus.FailureStatus;
20 import static com.android.adservices.service.measurement.reporting.ReportingStatus.UploadStatus;
21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_NETWORK_ERROR;
22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR;
23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_UNKNOWN_ERROR;
24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT;
25 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MESUREMENT_REPORTS_UPLOADED;
26 
27 import android.content.Context;
28 import android.net.Uri;
29 
30 import com.android.adservices.LoggerFactory;
31 import com.android.adservices.data.measurement.DatastoreManager;
32 import com.android.adservices.data.measurement.IMeasurementDao;
33 import com.android.adservices.errorlogging.ErrorLogUtil;
34 import com.android.adservices.service.Flags;
35 import com.android.adservices.service.measurement.KeyValueData;
36 import com.android.adservices.service.stats.AdServicesLogger;
37 import com.android.adservices.service.stats.MeasurementReportsStats;
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import org.json.JSONArray;
41 import org.json.JSONException;
42 
43 import java.io.IOException;
44 import java.net.HttpURLConnection;
45 import java.util.List;
46 import java.util.Optional;
47 import java.util.concurrent.ThreadLocalRandom;
48 
49 /** Class for handling debug reporting. */
50 public class DebugReportingJobHandler {
51 
52     private static final int MAX_HTTP_SUCCESS_CODE = 299;
53     private static final String LOGGING_NAME = "DebugReportingJobHandler";
54     private final DatastoreManager mDatastoreManager;
55     private final Flags mFlags;
56     private ReportingStatus.UploadMethod mUploadMethod;
57     private AdServicesLogger mLogger;
58 
59     private Context mContext;
60 
61     @VisibleForTesting
DebugReportingJobHandler( DatastoreManager datastoreManager, Flags flags, AdServicesLogger logger, Context context)62     DebugReportingJobHandler(
63             DatastoreManager datastoreManager,
64             Flags flags,
65             AdServicesLogger logger,
66             Context context) {
67         this(
68                 datastoreManager,
69                 flags,
70                 logger,
71                 ReportingStatus.UploadMethod.UNKNOWN,
72                 context);
73     }
74 
DebugReportingJobHandler( DatastoreManager datastoreManager, Flags flags, AdServicesLogger logger, ReportingStatus.UploadMethod uploadMethod, Context context)75     DebugReportingJobHandler(
76             DatastoreManager datastoreManager,
77             Flags flags,
78             AdServicesLogger logger,
79             ReportingStatus.UploadMethod uploadMethod,
80             Context context) {
81         mDatastoreManager = datastoreManager;
82         mFlags = flags;
83         mLogger = logger;
84         mUploadMethod = uploadMethod;
85         mContext = context;
86     }
87 
88     /** Finds all debug reports and attempts to upload them individually. */
performScheduledPendingReports()89     void performScheduledPendingReports() {
90         Optional<List<String>> pendingDebugReports =
91                 mDatastoreManager.runInTransactionWithResult(IMeasurementDao::getDebugReportIds);
92         if (!pendingDebugReports.isPresent()) {
93             ReportUtil.logReportingFailure(LOGGING_NAME, "Pending Verbose Debug Reports not found");
94             return;
95         }
96 
97         List<String> pendingDebugReportIdsInWindow = pendingDebugReports.get();
98         for (String debugReportId : pendingDebugReportIdsInWindow) {
99             // If the job service's requirements specified at runtime are no longer met, the job
100             // service will interrupt this thread.  If the thread has been interrupted, it will exit
101             // early.
102             if (Thread.currentThread().isInterrupted()) {
103                 ReportUtil.logReportingFailure(LOGGING_NAME, "Thread interrupted, exiting early");
104                 return;
105             }
106 
107             ReportingStatus reportingStatus = new ReportingStatus();
108             if (mUploadMethod != null) {
109                 reportingStatus.setUploadMethod(mUploadMethod);
110             }
111             performReport(debugReportId, reportingStatus);
112             if (reportingStatus.getUploadStatus() == ReportingStatus.UploadStatus.FAILURE) {
113                 mDatastoreManager.runInTransaction(
114                         (dao) -> {
115                             int retryCount =
116                                     dao.incrementAndGetReportingRetryCount(
117                                             debugReportId,
118                                             KeyValueData.DataType.DEBUG_REPORT_RETRY_COUNT);
119                             reportingStatus.setRetryCount(retryCount);
120                         });
121             }
122         }
123     }
124 
125     /**
126      * Perform reporting by finding the relevant {@link DebugReport} and making an HTTP POST request
127      * to the specified report to URL with the report data as a JSON in the body.
128      *
129      * @param debugReportId for the datastore id of the {@link DebugReport}
130      */
performReport(String debugReportId, ReportingStatus reportingStatus)131     void performReport(String debugReportId, ReportingStatus reportingStatus) {
132         String enrollmentId = null;
133         Optional<DebugReport> debugReportOpt =
134                 mDatastoreManager.runInTransactionWithResult(
135                         (dao) -> dao.getDebugReport(debugReportId));
136 
137         if (debugReportOpt.isEmpty()) {
138             ReportUtil.logReportingFailure(
139                     LOGGING_NAME, "Unable to read Scheduled Verbose Debug Report from database");
140             reportingStatus.setReportType(ReportingStatus.ReportType.VERBOSE_DEBUG_UNKNOWN);
141             setAndLogReportingStatus(
142                     reportingStatus,
143                     UploadStatus.FAILURE,
144                     FailureStatus.REPORT_NOT_FOUND,
145                     enrollmentId);
146             return;
147         }
148 
149         DebugReport debugReport = debugReportOpt.get();
150         enrollmentId = debugReport.getEnrollmentId();
151         reportingStatus.setReportingDelay(
152                 System.currentTimeMillis() - debugReport.getInsertionTime());
153         reportingStatus.setReportType(debugReport.getType());
154         reportingStatus.setSourceRegistrant(getAppPackageName(debugReport));
155 
156         try {
157             Uri reportingOrigin = debugReport.getRegistrationOrigin();
158             JSONArray debugReportJsonPayload = createReportJsonPayload(debugReport);
159             int returnCode = makeHttpPostRequest(reportingOrigin, debugReportJsonPayload);
160 
161             // Code outside [200, 299] is a failure according to HTTP protocol.
162             if (returnCode < HttpURLConnection.HTTP_OK || returnCode > MAX_HTTP_SUCCESS_CODE) {
163                 ReportUtil.logReportingFailure(
164                         LOGGING_NAME,
165                         String.format(
166                                 "Sending verbose debug report resulted in non-success HTTP status"
167                                         + " code %s",
168                                 returnCode),
169                         debugReportId,
170                         enrollmentId,
171                         debugReport.getType());
172                 setAndLogReportingStatus(
173                         reportingStatus,
174                         UploadStatus.FAILURE,
175                         FailureStatus.UNSUCCESSFUL_HTTP_RESPONSE_CODE,
176                         enrollmentId);
177                 return;
178             }
179             LoggerFactory.getMeasurementLogger()
180                     .d(
181                             "DebugReportingJobHandler (SUCCESS): Verbose debug report sent!"
182                                     + " Report ID: %s, Enrollment ID: %s, Type: %s",
183                             debugReportId, enrollmentId, debugReport.getType());
184 
185             boolean success =
186                     mDatastoreManager.runInTransaction(
187                             (dao) -> {
188                                 dao.deleteDebugReport(debugReport.getId());
189                             });
190             if (!success) {
191                 ReportUtil.logReportingFailure(
192                         LOGGING_NAME,
193                         "Deleting verbose debug report from database failed",
194                         debugReportId,
195                         enrollmentId,
196                         debugReport.getType());
197                 setAndLogReportingStatus(
198                         reportingStatus,
199                         UploadStatus.FAILURE,
200                         FailureStatus.DATASTORE,
201                         enrollmentId);
202                 return;
203             }
204             LoggerFactory.getMeasurementLogger()
205                     .d(
206                             "DebugReportingJobHandler (SUCCESS): Verbose debug removed from"
207                                     + " database! Report ID: %s, Enrollment ID: %s, Type: %s",
208                             debugReportId, enrollmentId, debugReport.getType());
209             setAndLogReportingStatus(
210                     reportingStatus, UploadStatus.SUCCESS, FailureStatus.UNKNOWN, enrollmentId);
211 
212         } catch (IOException e) {
213             ReportUtil.logReportingFailure(
214                     LOGGING_NAME,
215                     "Network error occurred when attempting to deliver verbose debug report",
216                     e,
217                     debugReportId,
218                     enrollmentId,
219                     debugReport.getType());
220             ErrorLogUtil.e(
221                     e,
222                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_NETWORK_ERROR,
223                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
224             setAndLogReportingStatus(
225                     reportingStatus, UploadStatus.FAILURE, FailureStatus.NETWORK, enrollmentId);
226         } catch (JSONException e) {
227             ReportUtil.logReportingFailure(
228                     LOGGING_NAME,
229                     "Serialization error occurred at verbose debug report delivery",
230                     e,
231                     debugReportId,
232                     enrollmentId,
233                     debugReport.getType());
234             ErrorLogUtil.e(
235                     e,
236                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR,
237                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
238             setAndLogReportingStatus(
239                     reportingStatus,
240                     UploadStatus.FAILURE,
241                     FailureStatus.SERIALIZATION_ERROR,
242                     enrollmentId);
243             if (mFlags.getMeasurementEnableReportDeletionOnUnrecoverableException()) {
244                 // Unrecoverable state - delete the report.
245                 ReportUtil.logReportingFailure(
246                         LOGGING_NAME,
247                         "Unrecoverable state, deleting the verbose debug report",
248                         debugReportId,
249                         enrollmentId,
250                         debugReport.getType());
251                 mDatastoreManager.runInTransaction(dao -> dao.deleteDebugReport(debugReportId));
252             }
253             if (mFlags.getMeasurementEnableReportingJobsThrowJsonException()
254                     && ThreadLocalRandom.current().nextFloat()
255                             < mFlags.getMeasurementThrowUnknownExceptionSamplingRate()) {
256                 // JSONException is unexpected.
257                 throw new IllegalStateException(
258                         "Serialization error occurred at event report delivery", e);
259             }
260         } catch (Exception e) {
261             LoggerFactory.getMeasurementLogger()
262                     .e(
263                             e,
264                             "DebugReportingJobHandler: Exception occurred when"
265                                     + " attempting to deliver verbose debug report.");
266             LoggerFactory.getMeasurementLogger()
267                     .d(
268                             "%s Report ID: %s, Enrollment ID: %s, Type: %s",
269                             e.toString(), debugReportId, enrollmentId, debugReport.getType());
270             ErrorLogUtil.e(
271                     e,
272                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_UNKNOWN_ERROR,
273                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
274             setAndLogReportingStatus(
275                     reportingStatus, UploadStatus.FAILURE, FailureStatus.UNKNOWN, enrollmentId);
276             if (mFlags.getMeasurementEnableReportingJobsThrowUnaccountedException()
277                     && ThreadLocalRandom.current().nextFloat()
278                             < mFlags.getMeasurementThrowUnknownExceptionSamplingRate()) {
279                 throw e;
280             }
281         }
282     }
283 
284     /** Creates the JSON payload for the POST request from the DebugReport. */
285     @VisibleForTesting
createReportJsonPayload(DebugReport debugReport)286     JSONArray createReportJsonPayload(DebugReport debugReport) throws JSONException {
287         JSONArray debugReportJsonPayload = new JSONArray();
288         debugReportJsonPayload.put(debugReport.toPayloadJson());
289         return debugReportJsonPayload;
290     }
291 
292     /** Makes the POST request to the reporting URL. */
293     @VisibleForTesting
makeHttpPostRequest(Uri adTechDomain, JSONArray debugReportPayload)294     public int makeHttpPostRequest(Uri adTechDomain, JSONArray debugReportPayload)
295             throws IOException {
296         DebugReportSender debugReportSender = new DebugReportSender(mContext);
297         return debugReportSender.sendReport(adTechDomain, debugReportPayload);
298     }
299 
getAppPackageName(DebugReport debugReport)300     private String getAppPackageName(DebugReport debugReport) {
301         if (!mFlags.getMeasurementEnableAppPackageNameLogging()) {
302             return "";
303         }
304         Uri sourceRegistrant = debugReport.getRegistrant();
305         if (sourceRegistrant == null) {
306             LoggerFactory.getMeasurementLogger().d("Source registrant is null on debug report");
307             return "";
308         }
309         return sourceRegistrant.toString();
310     }
311 
setAndLogReportingStatus( ReportingStatus reportingStatus, ReportingStatus.UploadStatus uploadStatus, ReportingStatus.FailureStatus failureStatus, String enrollmentId)312     private void setAndLogReportingStatus(
313             ReportingStatus reportingStatus,
314             ReportingStatus.UploadStatus uploadStatus,
315             ReportingStatus.FailureStatus failureStatus,
316             String enrollmentId) {
317         reportingStatus.setFailureStatus(failureStatus);
318         reportingStatus.setUploadStatus(uploadStatus);
319         logReportingStats(reportingStatus, enrollmentId);
320     }
321 
logReportingStats(ReportingStatus reportingStatus, String enrollmentId)322     private void logReportingStats(ReportingStatus reportingStatus, String enrollmentId) {
323         mLogger.logMeasurementReports(
324                 new MeasurementReportsStats.Builder()
325                         .setCode(AD_SERVICES_MESUREMENT_REPORTS_UPLOADED)
326                         .setType(reportingStatus.getReportType().getValue())
327                         .setResultCode(reportingStatus.getUploadStatus().getValue())
328                         .setFailureType(reportingStatus.getFailureStatus().getValue())
329                         .setUploadMethod(reportingStatus.getUploadMethod().getValue())
330                         .setReportingDelay(reportingStatus.getReportingDelay())
331                         .setSourceRegistrant(reportingStatus.getSourceRegistrant())
332                         .setRetryCount(reportingStatus.getRetryCount())
333                         .build(),
334                 enrollmentId);
335     }
336 }
337