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