blob: 4ad143fecbb57c7e6c2f7b84fc3b1b63da7593f9 [file] [log] [blame]
/*
* Copyright (C) 2022 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.service.measurement;
import static com.android.adservices.service.measurement.attribution.BaseUriExtractor.getBaseUri;
import static com.android.adservices.service.measurement.attribution.TriggerContentProvider.TRIGGER_URI;
import android.adservices.measurement.DeletionRequest;
import android.adservices.measurement.IMeasurementCallback;
import android.adservices.measurement.RegistrationRequest;
import android.annotation.NonNull;
import android.annotation.WorkerThread;
import android.content.AttributionSource;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.RemoteException;
import com.android.adservices.LogUtil;
import com.android.adservices.data.measurement.DatastoreManager;
import com.android.adservices.data.measurement.DatastoreManagerFactory;
import com.android.adservices.service.measurement.registration.SourceFetcher;
import com.android.adservices.service.measurement.registration.SourceRegistration;
import com.android.adservices.service.measurement.registration.TriggerFetcher;
import com.android.adservices.service.measurement.registration.TriggerRegistration;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import javax.annotation.concurrent.ThreadSafe;
/**
*
* <p>This class is thread safe.
* @hide
*/
@ThreadSafe
@WorkerThread
public final class MeasurementImpl {
private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
private static volatile MeasurementImpl sMeasurementImpl;
private final DatastoreManager mDatastoreManager;
private final SourceFetcher mSourceFetcher;
private final TriggerFetcher mTriggerFetcher;
private final ContentResolver mContentResolver;
private static final String ANDROID_APP_SCHEME = "android-app://";
private MeasurementImpl(Context context) {
mContentResolver = context.getContentResolver();
mDatastoreManager = DatastoreManagerFactory.getDatastoreManager(context);
mSourceFetcher = new SourceFetcher();
mTriggerFetcher = new TriggerFetcher();
}
@VisibleForTesting
MeasurementImpl(ContentResolver contentResolver, DatastoreManager datastoreManager,
SourceFetcher sourceFetcher, TriggerFetcher triggerFetcher) {
mContentResolver = contentResolver;
mDatastoreManager = datastoreManager;
mSourceFetcher = sourceFetcher;
mTriggerFetcher = triggerFetcher;
}
/**
* Gets an instance of MeasurementImpl to be used.
*
* <p>If no instance has been initialized yet, a new one will be created. Otherwise, the
* existing instance will be returned.
*/
@NonNull
public static MeasurementImpl getInstance(Context context) {
if (sMeasurementImpl == null) {
synchronized (MeasurementImpl.class) {
if (sMeasurementImpl == null) {
sMeasurementImpl = new MeasurementImpl(context);
}
}
}
return sMeasurementImpl;
}
/**
* Implement a registration request, returning a result code.
*/
int register(@NonNull RegistrationRequest request, long requestTime) {
mReadWriteLock.readLock().lock();
try {
switch (request.getRegistrationType()) {
case RegistrationRequest.REGISTER_SOURCE: {
ArrayList<SourceRegistration> results = new ArrayList();
boolean success = mSourceFetcher.fetchSource(request, results);
LogUtil.d("MeasurementImpl: register: success=" + success);
if (success) {
insertSources(request, results, requestTime);
return IMeasurementCallback.RESULT_OK;
} else {
return IMeasurementCallback.RESULT_IO_ERROR;
}
}
case RegistrationRequest.REGISTER_TRIGGER: {
ArrayList<TriggerRegistration> results = new ArrayList();
boolean success = mTriggerFetcher.fetchTrigger(request, results);
LogUtil.d("MeasurementImpl: register: success=" + success);
if (success) {
insertTriggers(request, results, requestTime);
return IMeasurementCallback.RESULT_OK;
} else {
return IMeasurementCallback.RESULT_IO_ERROR;
}
}
default:
return IMeasurementCallback.RESULT_INVALID_ARGUMENT;
}
} finally {
mReadWriteLock.readLock().unlock();
}
}
/**
* Implement a deleteRegistrations request, returning a result code.
*/
int deleteRegistrations(@NonNull DeletionRequest request) {
mReadWriteLock.readLock().lock();
try {
final boolean deleteResult = mDatastoreManager.runInTransaction((dao) ->
dao.deleteMeasurementData(
getRegistrant(request.getAttributionSource()),
request.getOriginUri(),
request.getStart(),
request.getEnd()
)
);
return deleteResult
? IMeasurementCallback.RESULT_OK : IMeasurementCallback.RESULT_INTERNAL_ERROR;
} catch (NullPointerException | IllegalArgumentException e) {
LogUtil.e(e, "Delete registration received invalid parameters");
return IMeasurementCallback.RESULT_INVALID_ARGUMENT;
} finally {
mReadWriteLock.readLock().unlock();
}
}
/**
* Delete all records from a specific package.
*/
public void deletePackageRecords(Uri packageUri) {
Uri appUri = getAppUri(packageUri);
LogUtil.i("Deleting records for " + appUri);
mReadWriteLock.writeLock().lock();
try {
mDatastoreManager.runInTransaction((dao) ->
dao.deleteAppRecords(appUri));
} catch (NullPointerException | IllegalArgumentException e) {
LogUtil.e(e, "Delete package records received invalid parameters");
} finally {
mReadWriteLock.writeLock().unlock();
}
}
private void insertSources(
@NonNull RegistrationRequest request,
ArrayList<SourceRegistration> responseBasedRegistrations,
long sourceEventTime) {
for (SourceRegistration registration : responseBasedRegistrations) {
Source source = new Source.Builder()
.setEventId(registration.getSourceEventId())
.setAttributionSource(request.getTopOriginUri())
// Only first destination to avoid AdTechs change this
.setAttributionDestination(responseBasedRegistrations.get(0)
.getDestination())
.setReportTo(getBaseUri(registration.getReportingOrigin()))
.setRegistrant(getRegistrant(request.getAttributionSource()))
.setSourceType(getSourceType(request))
.setPriority(registration.getSourcePriority())
.setEventTime(sourceEventTime)
.setExpiryTime(sourceEventTime
+ TimeUnit.SECONDS.toMillis(registration.getExpiry()))
// Setting as TRUTHFULLY as default value for tests.
// This will be overwritten by getSourceEventReports.
.setAttributionMode(Source.AttributionMode.TRUTHFULLY)
.build();
List<EventReport> eventReports = getSourceEventReports(source);
mDatastoreManager.runInTransaction((dao) -> {
dao.insertSource(
/* sourceEventId */ source.getEventId(),
/* attributionSource */ source.getAttributionSource(),
/* attributionDestination */ source.getAttributionDestination(),
/* reportTo */ source.getReportTo(),
/* registrant */ source.getRegistrant(),
/* sourceEventTime */ source.getEventTime(),
/* expiryTime */ source.getExpiryTime(),
/* priority */ source.getPriority(),
/* sourceType */ source.getSourceType(),
/* attributionMode */ source.getAttributionMode());
for (EventReport report : eventReports) {
dao.insertEventReport(report);
}
});
}
}
@VisibleForTesting
List<EventReport> getSourceEventReports(Source source) {
List<Source.FakeReport> fakeReports = source.assignAttributionModeAndGenerateFakeReport();
return fakeReports.stream().map(fakeReport ->
new EventReport.Builder()
.setSourceId(source.getEventId())
.setReportTime(fakeReport.getReportingTime())
.setTriggerData(fakeReport.getTriggerData())
.setAttributionDestination(source.getAttributionDestination())
.setReportTo(source.getReportTo())
.setTriggerTime(0)
.setTriggerPriority(0)
.setTriggerDedupKey(null)
.setSourceType(source.getSourceType())
.setStatus(EventReport.Status.PENDING)
.build()
).collect(Collectors.toList());
}
private Source.SourceType getSourceType(RegistrationRequest request) {
return request.getInputEvent() == null
? Source.SourceType.EVENT : Source.SourceType.NAVIGATION;
}
private void insertTriggers(
@NonNull RegistrationRequest request,
ArrayList<TriggerRegistration> responseBasedRegistrations,
long triggerTime) {
for (TriggerRegistration registration : responseBasedRegistrations) {
mDatastoreManager.runInTransaction((dao) ->
dao.insertTrigger(
/* attributionDestination */ request.getTopOriginUri(),
/* reportTo */ getBaseUri(registration.getReportingOrigin()),
/* registrant */ getRegistrant(request.getAttributionSource()),
/* triggerTime */ triggerTime,
/* triggerData */ registration.getTriggerData(),
/* dedupKey */ registration.getDeduplicationKey(),
/* priority */ registration.getTriggerPriority()));
}
try (ContentProviderClient contentProviderClient =
mContentResolver.acquireContentProviderClient(TRIGGER_URI)) {
if (contentProviderClient != null) {
contentProviderClient.insert(TRIGGER_URI, null);
}
} catch (RemoteException e) {
LogUtil.e(e, "Trigger Content Provider invocation failed.");
}
}
private Uri getRegistrant(AttributionSource attributionSource) {
return Uri.parse(
ANDROID_APP_SCHEME + attributionSource.getPackageName());
}
private Uri getAppUri(Uri packageUri) {
return Uri.parse(ANDROID_APP_SCHEME + packageUri.getEncodedSchemeSpecificPart());
}
}