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