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