blob: 88650b0dbccda853222bf621e04eac0ea989afbf [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.adservices.data.measurement;
import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_DELETE_EXPIRED_WINDOW_MS;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.adservices.LogUtil;
import com.android.adservices.service.measurement.AdtechUrl;
import com.android.adservices.service.measurement.EventReport;
import com.android.adservices.service.measurement.PrivacyParams;
import com.android.adservices.service.measurement.Source;
import com.android.adservices.service.measurement.Trigger;
import com.android.adservices.service.measurement.attribution.BaseUriExtractor;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Data Access Object for the Measurement PPAPI module.
*/
class MeasurementDao implements IMeasurementDao {
private static final String TAG = "MeasurementDao";
private SQLTransaction mSQLTransaction;
@Override
public void setTransaction(ITransaction transaction) {
if (!(transaction instanceof SQLTransaction)) {
throw new IllegalArgumentException("transaction should be a SQLTransaction.");
}
mSQLTransaction = (SQLTransaction) transaction;
}
@Override
public void insertTrigger(@NonNull Uri attributionDestination, @NonNull Uri reportTo,
@NonNull Uri registrant, @NonNull Long triggerTime, @NonNull Long triggerData,
@Nullable Long dedupKey, @NonNull Long priority, @Nullable String aggregateTriggerData,
@Nullable String aggregateValues) throws DatastoreException {
validateNonNull(attributionDestination, reportTo, registrant, triggerTime, triggerData,
priority);
validateUri(attributionDestination, reportTo, registrant);
ContentValues values = new ContentValues();
values.put(MeasurementTables.TriggerContract.ID, UUID.randomUUID().toString());
values.put(MeasurementTables.TriggerContract.ATTRIBUTION_DESTINATION,
attributionDestination.toString());
values.put(MeasurementTables.TriggerContract.TRIGGER_TIME, triggerTime);
values.put(MeasurementTables.TriggerContract.EVENT_TRIGGER_DATA, triggerData);
values.put(MeasurementTables.TriggerContract.DEDUP_KEY, dedupKey);
values.put(MeasurementTables.TriggerContract.PRIORITY, priority);
values.put(MeasurementTables.TriggerContract.STATUS, Trigger.Status.PENDING);
values.put(MeasurementTables.TriggerContract.REPORT_TO, reportTo.toString());
values.put(MeasurementTables.TriggerContract.REGISTRANT, registrant.toString());
values.put(MeasurementTables.TriggerContract.AGGREGATE_TRIGGER_DATA, aggregateTriggerData);
values.put(MeasurementTables.TriggerContract.AGGREGATE_VALUES, aggregateValues);
long rowId = mSQLTransaction.getDatabase()
.insert(MeasurementTables.TriggerContract.TABLE,
/*nullColumnHack=*/null, values);
LogUtil.d("MeasurementDao: insertTrigger: rowId=" + rowId);
if (rowId == -1) {
throw new DatastoreException("Trigger insertion failed.");
}
}
@Override
public List<String> getPendingTriggerIds() throws DatastoreException {
try (Cursor cursor = mSQLTransaction.getDatabase().query(
MeasurementTables.TriggerContract.TABLE,
new String[]{MeasurementTables.TriggerContract.ID},
MeasurementTables.TriggerContract.STATUS + " = ? ",
new String[]{String.valueOf(Trigger.Status.PENDING)},
/*groupBy=*/null, /*having=*/null,
/*orderBy=*/MeasurementTables.TriggerContract.TRIGGER_TIME, /*limit=*/null)) {
List<String> result = new ArrayList<>();
while (cursor.moveToNext()) {
result.add(cursor.getString(/*columnIndex=*/0));
}
return result;
}
}
@Override
public Trigger getTrigger(String triggerId) throws DatastoreException {
try (Cursor cursor = mSQLTransaction.getDatabase().query(
MeasurementTables.TriggerContract.TABLE,
/*columns=*/null,
MeasurementTables.TriggerContract.ID + " = ? ",
new String[]{triggerId},
/*groupBy=*/null, /*having=*/null, /*orderBy=*/null, /*limit=*/null)) {
if (cursor.getCount() == 0) {
throw new DatastoreException("Trigger retrieval failed. Id: " + triggerId);
}
cursor.moveToNext();
return SqliteObjectMapper.constructTriggerFromCursor(cursor);
}
}
@Override
public EventReport getEventReport(String eventReportId) throws DatastoreException {
try (Cursor cursor = mSQLTransaction.getDatabase().query(
MeasurementTables.EventReportContract.TABLE,
null,
MeasurementTables.EventReportContract.ID + " = ? ",
new String[]{eventReportId},
null,
null,
null,
null)) {
if (cursor.getCount() == 0) {
throw new DatastoreException(
"EventReport retrieval failed. Id: " + eventReportId);
}
cursor.moveToNext();
return SqliteObjectMapper.constructEventReportFromCursor(cursor);
}
}
@Override
public void insertSource(@NonNull Long sourceEventId, @NonNull Uri attributionSource,
@NonNull Uri attributionDestination, @NonNull Uri reportTo, @NonNull Uri registrant,
@NonNull Long sourceEventTime, @NonNull Long expiryTime, @NonNull Long priority,
@NonNull Source.SourceType sourceType, @Source.AttributionMode int attributionMode,
@Nullable String aggregateSource, @Nullable String aggregateFilterData)
throws DatastoreException {
validateNonNull(sourceEventId, attributionSource, attributionDestination, reportTo,
registrant, sourceEventTime, expiryTime, priority, sourceType);
validateUri(attributionSource, attributionDestination, reportTo, registrant);
ContentValues values = new ContentValues();
values.put(MeasurementTables.SourceContract.ID, UUID.randomUUID().toString());
values.put(MeasurementTables.SourceContract.EVENT_ID, sourceEventId);
values.put(MeasurementTables.SourceContract.ATTRIBUTION_SOURCE,
attributionSource.toString());
values.put(MeasurementTables.SourceContract.ATTRIBUTION_DESTINATION,
attributionDestination.toString());
values.put(MeasurementTables.SourceContract.REPORT_TO, reportTo.toString());
values.put(MeasurementTables.SourceContract.EVENT_TIME, sourceEventTime);
values.put(MeasurementTables.SourceContract.EXPIRY_TIME, expiryTime);
values.put(MeasurementTables.SourceContract.PRIORITY, priority);
values.put(MeasurementTables.SourceContract.STATUS, Source.Status.ACTIVE);
values.put(MeasurementTables.SourceContract.SOURCE_TYPE, sourceType.name());
values.put(MeasurementTables.SourceContract.REGISTRANT, registrant.toString());
values.put(MeasurementTables.SourceContract.ATTRIBUTION_MODE, attributionMode);
values.put(MeasurementTables.SourceContract.AGGREGATE_SOURCE, aggregateSource);
values.put(MeasurementTables.SourceContract.FILTER_DATA, aggregateFilterData);
long rowId = mSQLTransaction.getDatabase()
.insert(MeasurementTables.SourceContract.TABLE,
/*nullColumnHack=*/null, values);
LogUtil.d("MeasurementDao: insertSource: rowId=" + rowId);
if (rowId == -1) {
throw new DatastoreException("Source insertion failed.");
}
}
@Override
public List<Source> getMatchingActiveSources(Trigger trigger) throws DatastoreException {
try (Cursor cursor = mSQLTransaction.getDatabase().query(
MeasurementTables.SourceContract.TABLE,
/*columns=*/null,
MeasurementTables.SourceContract.ATTRIBUTION_DESTINATION + " = ? AND "
+ MeasurementTables.SourceContract.REPORT_TO + " = ? AND "
// EventTime should be strictly less than TriggerTime as it is highly
// unlikely for matching Source and Trigger to happen at same instant
// in milliseconds.
+ MeasurementTables.SourceContract.EVENT_TIME + " < ? AND "
+ MeasurementTables.SourceContract.EXPIRY_TIME + " >= ? AND "
+ MeasurementTables.SourceContract.STATUS + " != ?",
new String[]{
trigger.getAttributionDestination().toString(),
trigger.getReportTo().toString(),
String.valueOf(trigger.getTriggerTime()),
String.valueOf(trigger.getTriggerTime()),
String.valueOf(Source.Status.IGNORED)
},
/*groupBy=*/null, /*having=*/null, /*orderBy=*/null, /*limit=*/null)) {
List<Source> sources = new ArrayList<>();
while (cursor.moveToNext()) {
sources.add(SqliteObjectMapper.constructSourceFromCursor(cursor));
}
return sources;
}
}
@Override
public void updateTriggerStatus(Trigger trigger) throws DatastoreException {
ContentValues values = new ContentValues();
values.put(MeasurementTables.TriggerContract.STATUS, trigger.getStatus());
long rows = mSQLTransaction.getDatabase()
.update(MeasurementTables.TriggerContract.TABLE, values,
MeasurementTables.TriggerContract.ID + " = ?",
new String[]{trigger.getId()});
if (rows != 1) {
throw new DatastoreException("Trigger status update failed.");
}
}
@Override
public void updateSourceStatus(List<Source> sources, @Source.Status int status)
throws DatastoreException {
ContentValues values = new ContentValues();
values.put(MeasurementTables.SourceContract.STATUS, status);
long rows = mSQLTransaction.getDatabase()
.update(MeasurementTables.SourceContract.TABLE, values,
MeasurementTables.SourceContract.ID + " IN ("
+ Stream.generate(() -> "?").limit(sources.size())
.collect(Collectors.joining(",")) + ")",
sources.stream().map(Source::getId).toArray(String[]::new)
);
if (rows != sources.size()) {
throw new DatastoreException("Source status update failed.");
}
}
@Override
public void markEventReportDelivered(String eventReportId) throws DatastoreException {
ContentValues values = new ContentValues();
values.put(MeasurementTables.EventReportContract.STATUS, EventReport.Status.DELIVERED);
long rows = mSQLTransaction.getDatabase()
.update(MeasurementTables.EventReportContract.TABLE, values,
MeasurementTables.EventReportContract.ID + " = ?",
new String[]{eventReportId});
if (rows != 1) {
throw new DatastoreException("EventReport update failed.");
}
}
@Override
@Nullable
public List<EventReport> getSourceEventReports(Source source) throws DatastoreException {
List<EventReport> eventReports = new ArrayList<>();
try (Cursor cursor = mSQLTransaction.getDatabase().query(
MeasurementTables.EventReportContract.TABLE,
/*columns=*/null,
MeasurementTables.EventReportContract.SOURCE_ID + " = ? ",
new String[]{String.valueOf(source.getEventId())},
/*groupBy=*/null, /*having=*/null, /*orderBy=*/null, /*limit=*/null)) {
while (cursor.moveToNext()) {
eventReports.add(SqliteObjectMapper.constructEventReportFromCursor(cursor));
}
return eventReports;
}
}
@Override
public void deleteEventReport(EventReport eventReport) throws DatastoreException {
long rows = mSQLTransaction.getDatabase()
.delete(MeasurementTables.EventReportContract.TABLE,
MeasurementTables.EventReportContract.ID + " = ?",
new String[]{eventReport.getId()});
if (rows != 1) {
throw new DatastoreException("EventReport deletion failed.");
}
}
@Override
public List<String> getPendingEventReportIdsInWindow(long windowStartTime, long windowEndTime)
throws DatastoreException {
List<String> eventReports = new ArrayList<>();
try (Cursor cursor = mSQLTransaction.getDatabase().query(
MeasurementTables.EventReportContract.TABLE,
/*columns=*/null,
MeasurementTables.EventReportContract.REPORT_TIME + " >= ? AND "
+ MeasurementTables.EventReportContract.REPORT_TIME + " <= ? AND "
+ MeasurementTables.EventReportContract.STATUS + " = ? ",
new String[]{String.valueOf(windowStartTime), String.valueOf(windowEndTime),
String.valueOf(EventReport.Status.PENDING)},
/*groupBy=*/null, /*having=*/null, /*orderBy=*/"RANDOM()", /*limit=*/null)) {
while (cursor.moveToNext()) {
eventReports.add(cursor.getString(cursor.getColumnIndex(
MeasurementTables.EventReportContract.ID)));
}
return eventReports;
}
}
@Override
public List<String> getPendingEventReportIdsForGivenApp(Uri appName)
throws DatastoreException {
List<String> eventReports = new ArrayList<>();
try (Cursor cursor = mSQLTransaction.getDatabase().rawQuery(
String.format("SELECT e.%1$s FROM %2$s e "
+ "INNER JOIN %3$s s ON (e.%4$s = s.%5$s) "
+ "WHERE e.%6$s = ? AND s.%7$s = ?",
MeasurementTables.EventReportContract.ID,
MeasurementTables.EventReportContract.TABLE,
MeasurementTables.SourceContract.TABLE,
MeasurementTables.EventReportContract.SOURCE_ID,
MeasurementTables.SourceContract.EVENT_ID,
MeasurementTables.EventReportContract.STATUS,
MeasurementTables.SourceContract.REGISTRANT),
new String[]{String.valueOf(EventReport.Status.PENDING),
String.valueOf(appName)})) {
while (cursor.moveToNext()) {
eventReports.add(cursor.getString(cursor.getColumnIndex(
MeasurementTables.EventReportContract.ID)));
}
return eventReports;
}
}
@Override
public void insertEventReport(EventReport eventReport) throws DatastoreException {
ContentValues values = new ContentValues();
values.put(MeasurementTables.EventReportContract.ID,
UUID.randomUUID().toString());
values.put(MeasurementTables.EventReportContract.SOURCE_ID,
eventReport.getSourceId());
values.put(MeasurementTables.EventReportContract.ATTRIBUTION_DESTINATION,
eventReport.getAttributionDestination().toString());
values.put(MeasurementTables.EventReportContract.TRIGGER_TIME,
eventReport.getTriggerTime());
values.put(MeasurementTables.EventReportContract.TRIGGER_DATA,
eventReport.getTriggerData());
values.put(MeasurementTables.EventReportContract.TRIGGER_DEDUP_KEY,
eventReport.getTriggerDedupKey());
values.put(MeasurementTables.EventReportContract.REPORT_TO,
eventReport.getReportTo().toString());
values.put(MeasurementTables.EventReportContract.STATUS,
eventReport.getStatus());
values.put(MeasurementTables.EventReportContract.REPORT_TIME,
eventReport.getReportTime());
values.put(MeasurementTables.EventReportContract.TRIGGER_PRIORITY,
eventReport.getTriggerPriority());
values.put(MeasurementTables.EventReportContract.SOURCE_TYPE,
eventReport.getSourceType().toString());
long rowId = mSQLTransaction.getDatabase()
.insert(MeasurementTables.EventReportContract.TABLE,
/*nullColumnHack=*/null, values);
if (rowId == -1) {
throw new DatastoreException("EventReport insertion failed.");
}
}
@Override
public void updateSourceDedupKeys(Source source) throws DatastoreException {
ContentValues values = new ContentValues();
values.put(MeasurementTables.SourceContract.DEDUP_KEYS,
source.getDedupKeys().stream().map(String::valueOf).collect(
Collectors.joining(",")));
long rows = mSQLTransaction.getDatabase()
.update(MeasurementTables.SourceContract.TABLE, values,
MeasurementTables.SourceContract.ID + " = ?",
new String[]{source.getId()});
if (rows != 1) {
throw new DatastoreException("Source dedup key updated failed.");
}
}
@Override
public void insertAttributionRateLimit(Source source, Trigger trigger)
throws DatastoreException {
ContentValues values = new ContentValues();
values.put(MeasurementTables.AttributionRateLimitContract.ID,
UUID.randomUUID().toString());
values.put(MeasurementTables.AttributionRateLimitContract.SOURCE_SITE,
BaseUriExtractor.getBaseUri(source.getAttributionSource()).toString());
values.put(MeasurementTables.AttributionRateLimitContract.DESTINATION_SITE,
BaseUriExtractor.getBaseUri(trigger.getAttributionDestination()).toString());
values.put(MeasurementTables.AttributionRateLimitContract.REPORT_TO,
trigger.getReportTo().toString());
values.put(MeasurementTables.AttributionRateLimitContract.TRIGGER_TIME,
trigger.getTriggerTime());
values.put(MeasurementTables.AttributionRateLimitContract.REGISTRANT,
trigger.getRegistrant().toString());
long rowId = mSQLTransaction.getDatabase()
.insert(MeasurementTables.AttributionRateLimitContract.TABLE,
/*nullColumnHack=*/null,
values);
if (rowId == -1) {
throw new DatastoreException("AttributionRateLimit insertion failed.");
}
}
@Override
public long getAttributionsPerRateLimitWindow(Source source, Trigger trigger)
throws DatastoreException {
return DatabaseUtils.queryNumEntries(
mSQLTransaction.getDatabase(),
MeasurementTables.AttributionRateLimitContract.TABLE,
MeasurementTables.AttributionRateLimitContract.SOURCE_SITE + " = ? AND "
+ MeasurementTables.AttributionRateLimitContract.DESTINATION_SITE
+ " = ? AND "
+ MeasurementTables.AttributionRateLimitContract.REPORT_TO
+ " = ? AND "
+ MeasurementTables.AttributionRateLimitContract.TRIGGER_TIME
+ " >= ? AND "
+ MeasurementTables.AttributionRateLimitContract.TRIGGER_TIME
+ " <= ? ",
new String[]{
BaseUriExtractor.getBaseUri(source.getAttributionSource()).toString(),
BaseUriExtractor.getBaseUri(trigger.getAttributionDestination()).toString(),
trigger.getReportTo().toString(),
String.valueOf(trigger.getTriggerTime()
- PrivacyParams.RATE_LIMIT_WINDOW_MILLISECONDS),
String.valueOf(trigger.getTriggerTime())}
);
}
@Override
public long getNumSourcesPerRegistrant(Uri registrant) throws DatastoreException {
return DatabaseUtils.queryNumEntries(
mSQLTransaction.getDatabase(),
MeasurementTables.SourceContract.TABLE,
MeasurementTables.SourceContract.REGISTRANT + " = ? ",
new String[]{registrant.toString()});
}
@Override
public long getNumTriggersPerRegistrant(Uri registrant) throws DatastoreException {
return DatabaseUtils.queryNumEntries(
mSQLTransaction.getDatabase(),
MeasurementTables.TriggerContract.TABLE,
MeasurementTables.TriggerContract.REGISTRANT + " = ? ",
new String[]{registrant.toString()});
}
@Override
public void deleteAppRecords(Uri uri) throws DatastoreException {
String uriStr = uri.toString();
SQLiteDatabase db = mSQLTransaction.getDatabase();
// For all Source records matching the given Uri
// as REGISTRANT, obtains EventReport records who's SOURCE_ID
// matches a Source records' EVENT_ID.
db.delete(MeasurementTables.EventReportContract.TABLE,
String.format("%1$s IN ("
+ "SELECT e.%1$s FROM %2$s e"
+ " INNER JOIN %3$s s"
+ " ON (e.%4$s = s.%5$s AND e.%6$s = s.%7$s AND e.%8$s = s.%9$s)"
+ " WHERE s.%10$s = ?"
+ ")",
MeasurementTables.EventReportContract.ID,
MeasurementTables.EventReportContract.TABLE,
MeasurementTables.SourceContract.TABLE,
MeasurementTables.EventReportContract.SOURCE_ID,
MeasurementTables.SourceContract.EVENT_ID,
MeasurementTables.EventReportContract.ATTRIBUTION_DESTINATION,
MeasurementTables.SourceContract.ATTRIBUTION_DESTINATION,
MeasurementTables.EventReportContract.REPORT_TO,
MeasurementTables.SourceContract.REPORT_TO,
MeasurementTables.SourceContract.REGISTRANT),
new String[]{uriStr});
// EventReport table
db.delete(MeasurementTables.EventReportContract.TABLE,
MeasurementTables.EventReportContract.ATTRIBUTION_DESTINATION + " = ?",
new String[]{uriStr});
// Source table
db.delete(MeasurementTables.SourceContract.TABLE,
"( " + MeasurementTables.SourceContract.REGISTRANT + " = ? ) OR "
+ "(" + MeasurementTables.SourceContract.STATUS + " = ? AND "
+ MeasurementTables.SourceContract.ATTRIBUTION_DESTINATION + " = ? )",
new String[]{uriStr, String.valueOf(Source.Status.IGNORED), uriStr});
// Trigger table
db.delete(MeasurementTables.TriggerContract.TABLE,
MeasurementTables.TriggerContract.REGISTRANT + " = ?",
new String[]{uriStr});
// AttributionRateLimit table
db.delete(MeasurementTables.AttributionRateLimitContract.TABLE,
MeasurementTables.AttributionRateLimitContract.SOURCE_SITE + " = ? OR "
+ MeasurementTables.AttributionRateLimitContract.DESTINATION_SITE + " = ?",
new String[]{uriStr, uriStr});
}
@Override
@Nullable
public AdtechUrl getAdtechEnrollmentData(String postbackUrl) throws DatastoreException {
try (Cursor cursor = mSQLTransaction.getDatabase()
.query(MeasurementTables.AdTechUrlsContract.TABLE,
/*columns=*/null,
MeasurementTables.AdTechUrlsContract.POSTBACK_URL + " = ? ",
new String[]{postbackUrl},
/*groupBy=*/null, /*having=*/null, /*orderBy=*/null,
/*limit=*/null)) {
if (cursor == null || cursor.getCount() == 0) {
return null;
}
cursor.moveToNext();
return SqliteObjectMapper.constructAdtechUrlFromCursor(cursor);
}
}
@Override
public List<String> getAllAdtechUrls(String postbackUrl) throws DatastoreException {
List<String> res = new ArrayList<>();
AdtechUrl adtechUrl = getAdtechEnrollmentData(postbackUrl);
if (adtechUrl == null) {
return res;
}
String adtechId = adtechUrl.getAdtechId();
if (adtechId == null) {
return res;
}
try (Cursor cursor = mSQLTransaction.getDatabase()
.query(MeasurementTables.AdTechUrlsContract.TABLE,
/*columns=*/null,
MeasurementTables.AdTechUrlsContract.AD_TECH_ID + " = ? ",
new String[]{adtechId},
/*groupBy=*/null, /*having=*/null, /*orderBy=*/null,
/*limit=*/null)) {
if (cursor == null) {
return res;
}
while (cursor.moveToNext()) {
res.add(SqliteObjectMapper.constructAdtechUrlFromCursor(cursor).getPostbackUrl());
}
return res;
}
}
@Override
public void insertAdtechUrl(AdtechUrl adtechUrl) throws DatastoreException {
ContentValues values = new ContentValues();
values.put(MeasurementTables.AdTechUrlsContract.POSTBACK_URL, adtechUrl.getPostbackUrl());
values.put(MeasurementTables.AdTechUrlsContract.AD_TECH_ID, adtechUrl.getAdtechId());
long rowId = mSQLTransaction.getDatabase()
.insert(MeasurementTables.AdTechUrlsContract.TABLE,
/*nullColumnHack=*/null, values);
if (rowId == -1) {
throw new DatastoreException("AdTechURL insertion failed.");
}
}
@Override
public void deleteAdtechUrl(String postbackUrl) throws DatastoreException {
long rows = mSQLTransaction.getDatabase()
.delete(MeasurementTables.AdTechUrlsContract.TABLE,
MeasurementTables.AdTechUrlsContract.POSTBACK_URL + " = ?",
new String[]{postbackUrl});
if (rows != 1) {
throw new DatastoreException("AdTechURL deletion failed.");
}
}
@Override
public void deleteExpiredRecords() throws DatastoreException {
SQLiteDatabase db = mSQLTransaction.getDatabase();
long earliestValidInsertion =
System.currentTimeMillis() - MEASUREMENT_DELETE_EXPIRED_WINDOW_MS;
String earliestValidInsertionStr = String.valueOf(earliestValidInsertion);
// Source table
db.delete(MeasurementTables.SourceContract.TABLE,
MeasurementTables.SourceContract.EVENT_TIME + " < ?",
new String[]{earliestValidInsertionStr});
// Trigger table
db.delete(MeasurementTables.TriggerContract.TABLE,
MeasurementTables.TriggerContract.TRIGGER_TIME + " < ?",
new String[]{earliestValidInsertionStr});
// EventReport table
db.delete(MeasurementTables.EventReportContract.TABLE,
MeasurementTables.EventReportContract.STATUS + " = ? OR "
+ MeasurementTables.EventReportContract.REPORT_TIME + " < ?",
new String[]{
String.valueOf(EventReport.Status.DELIVERED),
earliestValidInsertionStr});
// AttributionRateLimit table
db.delete(MeasurementTables.AttributionRateLimitContract.TABLE,
MeasurementTables.AttributionRateLimitContract.TRIGGER_TIME + " < ?",
new String[]{earliestValidInsertionStr});
}
@Override
public void deleteMeasurementData(
@NonNull Uri registrant,
@Nullable Uri origin,
@Nullable Instant start,
@Nullable Instant end) throws DatastoreException {
Objects.requireNonNull(registrant);
validateOptionalRange(start, end);
final SQLiteDatabase db = mSQLTransaction.getDatabase();
deleteMeasurementData(db, registrant, origin, start, end);
}
private void validateOptionalRange(Instant start, Instant end) {
if (start == null ^ end == null) {
throw new IllegalArgumentException(
"invalid range, both start and end dates must be provided if providing any");
}
if (start != null && start.isAfter(end)) {
throw new IllegalArgumentException(
"invalid range, start date must be equal or before end date");
}
}
private void deleteMeasurementData(
SQLiteDatabase db, Uri registrant, Uri origin, Instant start, Instant end) {
if (origin == null && start == null) {
// Deletes all measurement data
deleteAttributionRateLimitByRegistrant(db, registrant);
deleteEventReportByRegistrant(db, registrant);
deleteTriggerByRegistrant(db, registrant);
deleteSourceByRegistrant(db, registrant);
} else if (start == null) {
// Deletes all measurement data by uri
deleteAttributionRateLimitByRegistrantAndUri(db, registrant, origin);
deleteEventReportByRegistrantAndUri(db, registrant, origin);
deleteTriggerByRegistrantAndUri(db, registrant, origin);
deleteSourceByRegistrantAndUri(db, registrant, origin);
} else if (origin == null) {
// Deletes all measurement data by date range
deleteAttributionRateLimitByRegistrantAndRange(db, registrant, start, end);
deleteEventReportByRegistrantAndRange(db, registrant, start, end);
deleteTriggerByRegistrantAndRange(db, registrant, start, end);
deleteSourceByRegistrantAndRange(db, registrant, start, end);
} else {
// Deletes all measurement data by uri and date range
deleteAttributionRateLimitByRegistrantAndUriAndRange(
db, registrant, origin, start, end);
deleteEventReportByRegistrantAndUriAndRange(db, registrant, origin, start, end);
deleteTriggerByRegistrantAndUriAndRange(db, registrant, origin, start, end);
deleteSourceByRegistrantAndUriAndRange(db, registrant, origin, start, end);
}
}
private void deleteSourceByRegistrant(SQLiteDatabase db, Uri registrant) {
db.delete(MeasurementTables.SourceContract.TABLE,
MeasurementTables.SourceContract.REGISTRANT + " = ?",
new String[]{registrant.toString()});
}
private void deleteSourceByRegistrantAndUri(
SQLiteDatabase db, Uri registrant, Uri attributionSource) {
db.delete(MeasurementTables.SourceContract.TABLE,
MeasurementTables.SourceContract.REGISTRANT + " = ? AND "
+ MeasurementTables.SourceContract.ATTRIBUTION_SOURCE + " = ?",
new String[]{registrant.toString(), attributionSource.toString()});
}
private void deleteSourceByRegistrantAndRange(
SQLiteDatabase db, Uri registrant, Instant start, Instant end) {
db.delete(MeasurementTables.SourceContract.TABLE,
MeasurementTables.SourceContract.REGISTRANT + " = ? AND "
+ MeasurementTables.SourceContract.EVENT_TIME + " >= ? AND "
+ MeasurementTables.SourceContract.EVENT_TIME + " <= ?",
new String[]{
registrant.toString(),
String.valueOf(start.toEpochMilli()),
String.valueOf(end.toEpochMilli())
});
}
private void deleteSourceByRegistrantAndUriAndRange(
SQLiteDatabase db, Uri registrant, Uri attributionSource, Instant start, Instant end) {
db.delete(MeasurementTables.SourceContract.TABLE,
MeasurementTables.SourceContract.REGISTRANT + " = ? AND "
+ MeasurementTables.SourceContract.ATTRIBUTION_SOURCE + " = ? AND "
+ MeasurementTables.SourceContract.EVENT_TIME + " >= ? AND "
+ MeasurementTables.SourceContract.EVENT_TIME + " <= ?",
new String[]{
registrant.toString(),
attributionSource.toString(),
String.valueOf(start.toEpochMilli()),
String.valueOf(end.toEpochMilli())
});
}
private void deleteTriggerByRegistrant(SQLiteDatabase db, Uri registrant) {
db.delete(MeasurementTables.TriggerContract.TABLE,
MeasurementTables.TriggerContract.REGISTRANT + " = ?",
new String[]{registrant.toString()});
}
private void deleteTriggerByRegistrantAndUri(
SQLiteDatabase db, Uri registrant, Uri attributionDestination) {
db.delete(MeasurementTables.TriggerContract.TABLE,
MeasurementTables.TriggerContract.REGISTRANT + " = ? AND "
+ MeasurementTables.TriggerContract.ATTRIBUTION_DESTINATION + " = ?",
new String[]{registrant.toString(), attributionDestination.toString()});
}
private void deleteTriggerByRegistrantAndRange(
SQLiteDatabase db, Uri registrant, Instant start, Instant end) {
db.delete(MeasurementTables.TriggerContract.TABLE,
MeasurementTables.TriggerContract.REGISTRANT + " = ? AND "
+ MeasurementTables.TriggerContract.TRIGGER_TIME + " >= ? AND "
+ MeasurementTables.TriggerContract.TRIGGER_TIME + " <= ?",
new String[]{
registrant.toString(),
String.valueOf(start.toEpochMilli()),
String.valueOf(end.toEpochMilli())
});
}
private void deleteTriggerByRegistrantAndUriAndRange(
SQLiteDatabase db,
Uri registrant,
Uri attributionDestination,
Instant start,
Instant end) {
db.delete(MeasurementTables.TriggerContract.TABLE,
MeasurementTables.TriggerContract.REGISTRANT + " = ? AND "
+ MeasurementTables.TriggerContract.ATTRIBUTION_DESTINATION + " = ? AND "
+ MeasurementTables.TriggerContract.TRIGGER_TIME + " >= ? AND "
+ MeasurementTables.TriggerContract.TRIGGER_TIME + " <= ?",
new String[]{
registrant.toString(),
attributionDestination.toString(),
String.valueOf(start.toEpochMilli()),
String.valueOf(end.toEpochMilli())
});
}
private void deleteEventReportByRegistrant(SQLiteDatabase db, Uri registrant) {
db.delete(MeasurementTables.EventReportContract.TABLE,
String.format("%1$s IN ("
+ "SELECT e.%1$s FROM %2$s e "
+ "INNER JOIN %3$s s ON (e.%4$s = s.%5$s) "
+ "WHERE %6$s = ?"
+ ")",
MeasurementTables.EventReportContract.ID,
MeasurementTables.EventReportContract.TABLE,
MeasurementTables.SourceContract.TABLE,
MeasurementTables.EventReportContract.SOURCE_ID,
MeasurementTables.SourceContract.EVENT_ID,
MeasurementTables.SourceContract.REGISTRANT),
new String[]{registrant.toString()});
}
private void deleteEventReportByRegistrantAndUri(SQLiteDatabase db, Uri registrant, Uri site) {
db.delete(MeasurementTables.EventReportContract.TABLE,
String.format("%1$s IN ("
+ "SELECT e.%1$s FROM %2$s e "
+ "INNER JOIN %3$s s ON (e.%4$s = s.%5$s) "
+ "WHERE s.%6$s = ? AND (s.%7$s = ? OR e.%8$s = ?)"
+ ")",
MeasurementTables.EventReportContract.ID,
MeasurementTables.EventReportContract.TABLE,
MeasurementTables.SourceContract.TABLE,
MeasurementTables.EventReportContract.SOURCE_ID,
MeasurementTables.SourceContract.EVENT_ID,
MeasurementTables.SourceContract.REGISTRANT,
MeasurementTables.SourceContract.ATTRIBUTION_SOURCE,
MeasurementTables.EventReportContract.ATTRIBUTION_DESTINATION),
new String[]{registrant.toString(), site.toString(), site.toString()});
}
private void deleteEventReportByRegistrantAndRange(
SQLiteDatabase db, Uri registrant, Instant start, Instant end) {
final String startValue = String.valueOf(start.toEpochMilli());
final String endValue = String.valueOf(end.toEpochMilli());
db.delete(MeasurementTables.EventReportContract.TABLE,
String.format("%1$s IN ("
+ "SELECT e.%1$s FROM %2$s e "
+ "INNER JOIN %3$s s ON (e.%4$s = s.%5$s) "
+ "WHERE %6$s = ? AND "
+ "((%7$s >= ? AND %7$s <= ?) OR (%8$s >= ? AND %8$s <= ?))"
+ ")",
MeasurementTables.EventReportContract.ID,
MeasurementTables.EventReportContract.TABLE,
MeasurementTables.SourceContract.TABLE,
MeasurementTables.EventReportContract.SOURCE_ID,
MeasurementTables.SourceContract.EVENT_ID,
MeasurementTables.SourceContract.REGISTRANT,
MeasurementTables.SourceContract.EVENT_TIME,
MeasurementTables.EventReportContract.TRIGGER_TIME),
new String[]{
registrant.toString(),
startValue,
endValue,
startValue,
endValue
});
}
private void deleteEventReportByRegistrantAndUriAndRange(
SQLiteDatabase db, Uri registrant, Uri site, Instant start, Instant end) {
final String startValue = String.valueOf(start.toEpochMilli());
final String endValue = String.valueOf(end.toEpochMilli());
db.delete(MeasurementTables.EventReportContract.TABLE,
String.format("%1$s IN ("
+ "SELECT e.%1$s FROM %2$s e "
+ "INNER JOIN %3$s s ON (e.%4$s = s.%5$s) "
+ "WHERE s.%6$s = ? AND "
+ "((s.%7$s = ? AND s.%8$s >= ? AND s.%8$s <= ?) OR "
+ "(e.%9$s = ? AND e.%10$s >= ? AND e.%10$s <= ?))"
+ ")",
MeasurementTables.EventReportContract.ID,
MeasurementTables.EventReportContract.TABLE,
MeasurementTables.SourceContract.TABLE,
MeasurementTables.EventReportContract.SOURCE_ID,
MeasurementTables.SourceContract.EVENT_ID,
MeasurementTables.SourceContract.REGISTRANT,
MeasurementTables.SourceContract.ATTRIBUTION_SOURCE,
MeasurementTables.SourceContract.EVENT_TIME,
MeasurementTables.EventReportContract.ATTRIBUTION_DESTINATION,
MeasurementTables.EventReportContract.TRIGGER_TIME),
new String[]{
registrant.toString(),
site.toString(),
startValue,
endValue,
site.toString(),
startValue,
endValue
});
}
private void deleteAttributionRateLimitByRegistrant(SQLiteDatabase db, Uri registrant) {
db.delete(MeasurementTables.AttributionRateLimitContract.TABLE,
MeasurementTables.AttributionRateLimitContract.REGISTRANT + " = ?",
new String[]{registrant.toString()});
}
private void deleteAttributionRateLimitByRegistrantAndUri(
SQLiteDatabase db, Uri registrant, Uri site) {
db.delete(MeasurementTables.AttributionRateLimitContract.TABLE,
String.format("%1$s = ? AND (%2$s = ? OR %3$s = ?)",
MeasurementTables.AttributionRateLimitContract.REGISTRANT,
MeasurementTables.AttributionRateLimitContract.SOURCE_SITE,
MeasurementTables.AttributionRateLimitContract.DESTINATION_SITE),
new String[]{registrant.toString(), site.toString(), site.toString()});
}
private void deleteAttributionRateLimitByRegistrantAndRange(
SQLiteDatabase db, Uri registrant, Instant start, Instant end) {
db.delete(MeasurementTables.AttributionRateLimitContract.TABLE,
MeasurementTables.AttributionRateLimitContract.REGISTRANT + " = ? AND "
+ MeasurementTables.AttributionRateLimitContract.TRIGGER_TIME + " >= ? AND "
+ MeasurementTables.AttributionRateLimitContract.TRIGGER_TIME + " <= ?",
new String[]{
registrant.toString(),
String.valueOf(start.toEpochMilli()),
String.valueOf(end.toEpochMilli())});
}
private void deleteAttributionRateLimitByRegistrantAndUriAndRange(
SQLiteDatabase db, Uri registrant, Uri site, Instant start, Instant end) {
db.delete(MeasurementTables.AttributionRateLimitContract.TABLE,
String.format("%1$s = ? AND (%2$s = ? OR %3$s = ?) AND (%4$s >= ? AND %4$s <= ?)",
MeasurementTables.AttributionRateLimitContract.REGISTRANT,
MeasurementTables.AttributionRateLimitContract.SOURCE_SITE,
MeasurementTables.AttributionRateLimitContract.DESTINATION_SITE,
MeasurementTables.AttributionRateLimitContract.TRIGGER_TIME),
new String[]{
registrant.toString(),
site.toString(),
site.toString(),
String.valueOf(start.toEpochMilli()),
String.valueOf(end.toEpochMilli())});
}
@Override
public void doInstallAttribution(Uri uri, long eventTimestamp) throws DatastoreException {
SQLiteDatabase db = mSQLTransaction.getDatabase();
SQLiteQueryBuilder sqb = new SQLiteQueryBuilder();
sqb.setTables(MeasurementTables.SourceContract.TABLE);
// Sub query for selecting relevant source ids.
// Selecting the highest priority, most recent source with eventTimestamp falling in the
// source's install attribution window.
String subQuery = sqb.buildQuery(new String[]{MeasurementTables.SourceContract.ID},
String.format(MeasurementTables.SourceContract.ATTRIBUTION_DESTINATION
+ " = \"%s\" AND "
+ MeasurementTables.SourceContract.EVENT_TIME + " <= %2$d AND "
+ MeasurementTables.SourceContract.EXPIRY_TIME + " > %2$d AND "
+ MeasurementTables.SourceContract.EVENT_TIME + " + "
+ MeasurementTables.SourceContract.INSTALL_ATTRIBUTION_WINDOW
+ " >= %2$d",
uri.toString(), eventTimestamp),
/* groupBy= */null, /* having= */null,
/* sortOrder= */MeasurementTables.SourceContract.PRIORITY + " DESC, "
+ MeasurementTables.SourceContract.EVENT_TIME + " DESC",
/* limit = */ "1");
ContentValues values = new ContentValues();
values.put(MeasurementTables.SourceContract.IS_INSTALL_ATTRIBUTED, true);
db.update(MeasurementTables.SourceContract.TABLE,
values,
MeasurementTables.SourceContract.ID + " IN (" + subQuery + ")",
null);
}
@Override
public void undoInstallAttribution(Uri uri) throws DatastoreException {
SQLiteDatabase db = mSQLTransaction.getDatabase();
ContentValues values = new ContentValues();
values.put(MeasurementTables.SourceContract.IS_INSTALL_ATTRIBUTED, false);
db.update(MeasurementTables.SourceContract.TABLE,
values,
MeasurementTables.SourceContract.ATTRIBUTION_DESTINATION + " = ?",
new String[]{uri.toString()});
}
private void validateNonNull(Object... objects) throws DatastoreException {
for (Object o : objects) {
if (o == null) {
throw new DatastoreException("Received null values");
}
}
}
private void validateUri(Uri... uris) throws DatastoreException {
for (Uri uri : uris) {
if (uri == null || uri.getScheme() == null) {
throw new DatastoreException("Uri with no scheme is not valid");
}
}
}
}