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