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