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.data.measurement.deletion; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_WIPEOUT; 20 21 import android.adservices.measurement.DeletionParam; 22 import android.adservices.measurement.DeletionRequest; 23 import android.annotation.NonNull; 24 import android.net.Uri; 25 26 import com.android.adservices.LoggerFactory; 27 import com.android.adservices.data.measurement.DatastoreException; 28 import com.android.adservices.data.measurement.DatastoreManager; 29 import com.android.adservices.data.measurement.IMeasurementDao; 30 import com.android.adservices.service.Flags; 31 import com.android.adservices.service.measurement.EventReport; 32 import com.android.adservices.service.measurement.Source; 33 import com.android.adservices.service.measurement.Trigger; 34 import com.android.adservices.service.measurement.WipeoutStatus; 35 import com.android.adservices.service.measurement.aggregation.AggregateHistogramContribution; 36 import com.android.adservices.service.measurement.aggregation.AggregateReport; 37 import com.android.adservices.service.measurement.util.UnsignedLong; 38 import com.android.adservices.service.stats.AdServicesLogger; 39 import com.android.adservices.service.stats.AdServicesLoggerImpl; 40 import com.android.adservices.service.stats.MeasurementWipeoutStats; 41 import com.android.internal.annotations.VisibleForTesting; 42 43 import org.json.JSONArray; 44 import org.json.JSONException; 45 46 import java.time.Instant; 47 import java.util.Collections; 48 import java.util.List; 49 import java.util.Objects; 50 import java.util.Optional; 51 import java.util.Set; 52 53 /** 54 * Facilitates deletion of measurement data from the database, for e.g. deletion of sources, 55 * triggers, reports, attributions. 56 */ 57 public class MeasurementDataDeleter { 58 static final String ANDROID_APP_SCHEME = "android-app"; 59 private static final int AGGREGATE_CONTRIBUTIONS_VALUE_MINIMUM_LIMIT = 0; 60 private final DatastoreManager mDatastoreManager; 61 private final Flags mFlags; 62 private final AdServicesLogger mLogger; 63 MeasurementDataDeleter(DatastoreManager datastoreManager, Flags flags)64 public MeasurementDataDeleter(DatastoreManager datastoreManager, Flags flags) { 65 this(datastoreManager, flags, AdServicesLoggerImpl.getInstance()); 66 } 67 68 @VisibleForTesting MeasurementDataDeleter( DatastoreManager datastoreManager, Flags flags, AdServicesLogger logger)69 public MeasurementDataDeleter( 70 DatastoreManager datastoreManager, Flags flags, AdServicesLogger logger) { 71 mDatastoreManager = datastoreManager; 72 mFlags = flags; 73 mLogger = logger; 74 } 75 76 /** 77 * Deletes all measurement data owned by a registrant and optionally providing an origin uri 78 * and/or a range of dates. 79 * 80 * @param deletionParam contains registrant, time range, sites to consider for deletion 81 * @return true if deletion was successful, false otherwise 82 */ delete(@onNull DeletionParam deletionParam)83 public boolean delete(@NonNull DeletionParam deletionParam) { 84 boolean result = mDatastoreManager.runInTransaction((dao) -> delete(dao, deletionParam)); 85 if (result) { 86 // Log wipeout event triggered by request (from the delete registrations API) 87 WipeoutStatus wipeoutStatus = new WipeoutStatus(); 88 wipeoutStatus.setWipeoutType(WipeoutStatus.WipeoutType.DELETE_REGISTRATIONS_API); 89 logWipeoutStats( 90 wipeoutStatus, getRegistrant(deletionParam.getAppPackageName()).toString()); 91 } 92 return result; 93 } 94 95 /** 96 * Deletes all measurement data for a given package name that has been uninstalled. 97 * 98 * @param packageName including android-app:// scheme 99 * @return true if deletion deleted any record 100 */ deleteAppUninstalledData(@onNull Uri packageName)101 public boolean deleteAppUninstalledData(@NonNull Uri packageName) { 102 // Using MATCH_BEHAVIOR_PRESERVE with empty origins and domains to preserve nothing. 103 // In other words, to delete all data that only matches the provided app package name. 104 final DeletionParam deletionParam = 105 new DeletionParam.Builder( 106 /* originUris= */ Collections.emptyList(), 107 /* domainUris= */ Collections.emptyList(), 108 /* start= */ Instant.MIN, 109 /* end= */ Instant.MAX, 110 /* appPackageName= */ packageName.getHost(), 111 /* sdkPackageName= */ "") 112 .setMatchBehavior(DeletionRequest.MATCH_BEHAVIOR_PRESERVE) 113 .build(); 114 115 Optional<Boolean> result = 116 mDatastoreManager.runInTransactionWithResult( 117 (dao) -> { 118 dao.undoInstallAttribution(packageName); 119 return delete(dao, deletionParam); 120 }); 121 return result.orElse(false); 122 } 123 124 /** Returns true if any record were deleted. */ delete(@onNull IMeasurementDao dao, @NonNull DeletionParam deletionParam)125 private boolean delete(@NonNull IMeasurementDao dao, @NonNull DeletionParam deletionParam) 126 throws DatastoreException { 127 List<String> sourceIds = 128 dao.fetchMatchingSources( 129 getRegistrant(deletionParam.getAppPackageName()), 130 deletionParam.getStart(), 131 deletionParam.getEnd(), 132 deletionParam.getOriginUris(), 133 deletionParam.getDomainUris(), 134 deletionParam.getMatchBehavior()); 135 Set<String> triggerIds = 136 dao.fetchMatchingTriggers( 137 getRegistrant(deletionParam.getAppPackageName()), 138 deletionParam.getStart(), 139 deletionParam.getEnd(), 140 deletionParam.getOriginUris(), 141 deletionParam.getDomainUris(), 142 deletionParam.getMatchBehavior()); 143 List<String> asyncRegistrationIds = 144 dao.fetchMatchingAsyncRegistrations( 145 getRegistrant(deletionParam.getAppPackageName()), 146 deletionParam.getStart(), 147 deletionParam.getEnd(), 148 deletionParam.getOriginUris(), 149 deletionParam.getDomainUris(), 150 deletionParam.getMatchBehavior()); 151 152 int debugReportsDeletedCount = 153 dao.deleteDebugReports( 154 getRegistrant(deletionParam.getAppPackageName()), 155 deletionParam.getStart(), 156 deletionParam.getEnd()); 157 158 final boolean containsRecordsToBeDeleted = 159 !sourceIds.isEmpty() || !triggerIds.isEmpty() || !asyncRegistrationIds.isEmpty(); 160 if (!containsRecordsToBeDeleted) { 161 return debugReportsDeletedCount > 0; 162 } 163 164 // Reset aggregate contributions and dedup keys on sources for triggers to be 165 // deleted. 166 List<AggregateReport> aggregateReports = 167 dao.fetchMatchingAggregateReports(sourceIds, triggerIds); 168 resetAggregateContributions(dao, aggregateReports); 169 resetAggregateReportDedupKeys(dao, aggregateReports); 170 List<EventReport> eventReports; 171 if (mFlags.getMeasurementFlexibleEventReportingApiEnabled()) { 172 /* 173 Because some triggers may not be stored in the event report table in 174 the flexible event report API, we must extract additional related 175 triggers from the source table. 176 */ 177 Set<String> extendedSourceIds = dao.fetchFlexSourceIdsFor(triggerIds); 178 179 // IMeasurementDao::fetchFlexSourceIdsFor fetches only 180 // sources that have trigger specs (flex API), which means we can examine 181 // only their attributed trigger list. 182 for (String sourceId : extendedSourceIds) { 183 Source source = dao.getSource(sourceId); 184 try { 185 source.buildAttributedTriggers(); 186 triggerIds.addAll(source.getAttributedTriggerIds()); 187 // Delete all attributed triggers for the source. 188 dao.updateSourceAttributedTriggers(sourceId, new JSONArray().toString()); 189 } catch (JSONException error) { 190 LoggerFactory.getMeasurementLogger() 191 .e( 192 error, 193 "MeasurementDataDeleter::delete unable to build attributed " 194 + "triggers. Source ID: %s", 195 sourceId); 196 } 197 } 198 199 extendedSourceIds.addAll(sourceIds); 200 201 eventReports = dao.fetchMatchingEventReports(extendedSourceIds, triggerIds); 202 } else { 203 eventReports = dao.fetchMatchingEventReports(sourceIds, triggerIds); 204 } 205 206 resetDedupKeys(dao, eventReports); 207 208 dao.deleteAsyncRegistrations(asyncRegistrationIds); 209 210 // Delete sources and triggers, that'll take care of deleting related reports 211 // and attributions 212 if (deletionParam.getDeletionMode() == DeletionRequest.DELETION_MODE_ALL) { 213 dao.deleteSources(sourceIds); 214 dao.deleteTriggers(triggerIds); 215 return true; 216 } 217 218 // Mark reports for deletion for DELETION_MODE_EXCLUDE_INTERNAL_DATA 219 for (EventReport eventReport : eventReports) { 220 dao.markEventReportStatus(eventReport.getId(), EventReport.Status.MARKED_TO_DELETE); 221 } 222 223 for (AggregateReport aggregateReport : aggregateReports) { 224 dao.markAggregateReportStatus( 225 aggregateReport.getId(), AggregateReport.Status.MARKED_TO_DELETE); 226 } 227 228 // Finally mark sources and triggers for deletion 229 dao.updateSourceStatus(sourceIds, Source.Status.MARKED_TO_DELETE); 230 dao.updateTriggerStatus(triggerIds, Trigger.Status.MARKED_TO_DELETE); 231 return true; 232 } 233 234 @VisibleForTesting resetAggregateContributions( @onNull IMeasurementDao dao, @NonNull List<AggregateReport> aggregateReports)235 void resetAggregateContributions( 236 @NonNull IMeasurementDao dao, @NonNull List<AggregateReport> aggregateReports) 237 throws DatastoreException { 238 for (AggregateReport report : aggregateReports) { 239 if (report.getSourceId() == null) { 240 LoggerFactory.getMeasurementLogger().d("SourceId is null on event report."); 241 return; 242 } 243 244 Source source = dao.getSource(report.getSourceId()); 245 int aggregateHistogramContributionsSum = 246 report.extractAggregateHistogramContributions().stream() 247 .mapToInt(AggregateHistogramContribution::getValue) 248 .sum(); 249 250 int newAggregateContributionsSum = 251 Math.max( 252 (source.getAggregateContributions() 253 - aggregateHistogramContributionsSum), 254 AGGREGATE_CONTRIBUTIONS_VALUE_MINIMUM_LIMIT); 255 256 source.setAggregateContributions(newAggregateContributionsSum); 257 258 // Update in the DB 259 dao.updateSourceAggregateContributions(source); 260 } 261 } 262 263 @VisibleForTesting resetDedupKeys(@onNull IMeasurementDao dao, @NonNull List<EventReport> eventReports)264 void resetDedupKeys(@NonNull IMeasurementDao dao, @NonNull List<EventReport> eventReports) 265 throws DatastoreException { 266 for (EventReport report : eventReports) { 267 if (report.getSourceId() == null) { 268 LoggerFactory.getMeasurementLogger() 269 .d("resetDedupKeys: SourceId on the event report is null."); 270 continue; 271 } 272 273 Source source = dao.getSource(report.getSourceId()); 274 UnsignedLong dedupKey = report.getTriggerDedupKey(); 275 276 // Event reports for flex API do not have trigger dedup key populated. Otherwise, 277 // it may or may not be. 278 if (dedupKey == null) { 279 return; 280 } 281 282 if (mFlags.getMeasurementEnableAraDeduplicationAlignmentV1()) { 283 try { 284 source.buildAttributedTriggers(); 285 source.getAttributedTriggers().removeIf(attributedTrigger -> 286 dedupKey.equals(attributedTrigger.getDedupKey()) 287 && Objects.equals( 288 report.getTriggerId(), 289 attributedTrigger.getTriggerId())); 290 dao.updateSourceAttributedTriggers( 291 source.getId(), source.attributedTriggersToJson()); 292 } catch (JSONException e) { 293 LoggerFactory.getMeasurementLogger() 294 .e(e, "resetDedupKeys: failed to build attributed triggers."); 295 } 296 } else { 297 source.getEventReportDedupKeys().remove(dedupKey); 298 dao.updateSourceEventReportDedupKeys(source); 299 } 300 } 301 } 302 resetAggregateReportDedupKeys( @onNull IMeasurementDao dao, @NonNull List<AggregateReport> aggregateReports)303 void resetAggregateReportDedupKeys( 304 @NonNull IMeasurementDao dao, @NonNull List<AggregateReport> aggregateReports) 305 throws DatastoreException { 306 for (AggregateReport report : aggregateReports) { 307 if (report.getSourceId() == null) { 308 LoggerFactory.getMeasurementLogger().d("SourceId on the aggregate report is null."); 309 continue; 310 } 311 312 Source source = dao.getSource(report.getSourceId()); 313 if (report.getDedupKey() == null) { 314 continue; 315 } 316 source.getAggregateReportDedupKeys().remove(report.getDedupKey()); 317 dao.updateSourceAggregateReportDedupKeys(source); 318 } 319 } 320 getRegistrant(String packageName)321 private Uri getRegistrant(String packageName) { 322 return Uri.parse(ANDROID_APP_SCHEME + "://" + packageName); 323 } 324 logWipeoutStats(WipeoutStatus wipeoutStatus, String sourceRegistrant)325 private void logWipeoutStats(WipeoutStatus wipeoutStatus, String sourceRegistrant) { 326 mLogger.logMeasurementWipeoutStats( 327 new MeasurementWipeoutStats.Builder() 328 .setCode(AD_SERVICES_MEASUREMENT_WIPEOUT) 329 .setWipeoutType(wipeoutStatus.getWipeoutType().getValue()) 330 .setSourceRegistrant(sourceRegistrant) 331 .build()); 332 } 333 } 334