• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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