blob: 1eba340635f66ee83c29a264e18aa478a73eb418 [file] [log] [blame]
/*
* Copyright (C) 2023 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.adselection;
import android.adservices.adselection.AdWithBid;
import android.adservices.adselection.SignedContextualAds;
import android.adservices.common.AdTechIdentifier;
import android.adservices.common.FrequencyCapFilters;
import android.adservices.common.KeyedFrequencyCap;
import android.annotation.NonNull;
import com.android.adservices.LoggerFactory;
import com.android.adservices.data.adselection.AppInstallDao;
import com.android.adservices.data.adselection.FrequencyCapDao;
import com.android.adservices.data.common.DBAdData;
import com.android.adservices.data.customaudience.DBCustomAudience;
import com.android.adservices.service.profiling.Tracing;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** Holds filters to remove ads from the selectAds auction. */
public final class AdFiltererImpl implements AdFilterer {
private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
@NonNull private final Clock mClock;
@NonNull private final AppInstallDao mAppInstallDao;
@NonNull private final FrequencyCapDao mFrequencyCapDao;
public AdFiltererImpl(
@NonNull AppInstallDao appInstallDao,
@NonNull FrequencyCapDao frequencyCapDao,
@NonNull Clock clock) {
Objects.requireNonNull(appInstallDao);
Objects.requireNonNull(frequencyCapDao);
Objects.requireNonNull(clock);
mAppInstallDao = appInstallDao;
mFrequencyCapDao = frequencyCapDao;
mClock = clock;
}
/**
* Takes a list of CAs and returns an identical list with any ads that should be filtered
* removed.
*
* <p>Note that some of the copying to the new list is shallow, so the original list should not
* be re-used after the method is called.
*
* @param cas A list of CAs to filter ads for.
* @return A list of cas identical to the cas input, but with any ads that should be filtered
* removed.
*/
@Override
public List<DBCustomAudience> filterCustomAudiences(List<DBCustomAudience> cas) {
final int filterCATraceCookie = Tracing.beginAsyncSection(Tracing.FILTERER_FILTER_CA);
try {
List<DBCustomAudience> toReturn = new ArrayList<>();
Instant currentTime = mClock.instant();
sLogger.v("Applying filters to %d CAs with current time %s.", cas.size(), currentTime);
int totalAds = 0;
int remainingAds = 0;
for (DBCustomAudience ca : cas) {
final int forEachCATraceCookie =
Tracing.beginAsyncSection(Tracing.FILTERER_FOR_EACH_CA);
List<DBAdData> filteredAds = new ArrayList<>();
totalAds += ca.getAds().size();
for (DBAdData ad : ca.getAds()) {
if (doesAdPassFilters(
ad, ca.getBuyer(), ca.getOwner(), ca.getName(), currentTime)) {
filteredAds.add(ad);
}
}
if (!filteredAds.isEmpty()) {
toReturn.add(new DBCustomAudience.Builder(ca).setAds(filteredAds).build());
remainingAds += filteredAds.size();
}
Tracing.endAsyncSection(Tracing.FILTERER_FOR_EACH_CA, forEachCATraceCookie);
}
sLogger.v(
"Filtering finished. %d CAs of the original %d remain. "
+ "%d Ads of the original %d remain.",
toReturn.size(), cas.size(), remainingAds, totalAds);
return toReturn;
} finally {
Tracing.endAsyncSection(Tracing.FILTERER_FILTER_CA, filterCATraceCookie);
}
}
/**
* Takes in a {@link SignedContextualAds} object and filters out ads from it that should not be
* in the auction
*
* @param contextualAds An object containing contextual ads corresponding to a buyer
* @return A list of object identical to the input, but without any ads that should be filtered
*/
@Override
public SignedContextualAds filterContextualAds(SignedContextualAds contextualAds) {
final int traceCookie = Tracing.beginAsyncSection(Tracing.FILTERER_FILTER_CONTEXTUAL);
try {
List<AdWithBid> adsList = new ArrayList<>();
Instant currentTime = mClock.instant();
sLogger.v(
"Applying filters to %d contextual ads with current time %s.",
contextualAds.getAdsWithBid().size(), currentTime);
for (AdWithBid ad : contextualAds.getAdsWithBid()) {
DBAdData dbAdData = new DBAdData.Builder(ad.getAdData()).build();
if (doesAdPassFilters(
dbAdData, contextualAds.getBuyer(), null, null, currentTime)) {
adsList.add(ad);
}
}
sLogger.v(
"Filtering finished. %d contextual ads of the original %d remain.",
adsList.size(), contextualAds.getAdsWithBid().size());
return contextualAds.cloneToBuilder().setAdsWithBid(adsList).build();
} finally {
Tracing.endAsyncSection(Tracing.FILTERER_FILTER_CONTEXTUAL, traceCookie);
}
}
private boolean doesAdPassFilters(
DBAdData ad,
AdTechIdentifier buyer,
String customAudienceOwner,
String customAudienceName,
Instant currentTime) {
if (ad.getAdFilters() == null) {
return true;
}
return doesAdPassAppInstallFilters(ad, buyer)
&& doesAdPassFrequencyCapFilters(
ad, buyer, customAudienceOwner, customAudienceName, currentTime);
}
private boolean doesAdPassAppInstallFilters(DBAdData ad, AdTechIdentifier buyer) {
/* This could potentially be optimized by grouping the ads by package name before running
* the queries, but unless the DB cache is playing poorly with these queries there might
* not be a major performance improvement.
*/
if (ad.getAdFilters().getAppInstallFilters() == null) {
return true;
}
for (String packageName : ad.getAdFilters().getAppInstallFilters().getPackageNames()) {
if (mAppInstallDao.canBuyerFilterPackage(buyer, packageName)) {
return false;
}
}
return true;
}
private boolean doesAdPassFrequencyCapFilters(
DBAdData ad,
AdTechIdentifier buyer,
String customAudienceOwner,
String customAudienceName,
Instant currentTime) {
if (ad.getAdFilters().getFrequencyCapFilters() == null) {
return true;
}
FrequencyCapFilters filters = ad.getAdFilters().getFrequencyCapFilters();
// TODO(b/265205439): Compare the performance of loading the histograms once for each custom
// audience and buyer versus querying for every filter
// Contextual ads cannot filter on win-typed events
boolean adIsFromCustomAudience =
(customAudienceOwner != null) && (customAudienceName != null);
if (adIsFromCustomAudience
&& !filters.getKeyedFrequencyCapsForWinEvents().isEmpty()
&& !doesAdPassFrequencyCapFiltersForWinType(
filters.getKeyedFrequencyCapsForWinEvents(),
buyer,
customAudienceOwner,
customAudienceName,
currentTime)) {
return false;
}
if (!filters.getKeyedFrequencyCapsForImpressionEvents().isEmpty()
&& !doesAdPassFrequencyCapFiltersForNonWinType(
filters.getKeyedFrequencyCapsForImpressionEvents(),
FrequencyCapFilters.AD_EVENT_TYPE_IMPRESSION,
buyer,
currentTime)) {
return false;
}
if (!filters.getKeyedFrequencyCapsForViewEvents().isEmpty()
&& !doesAdPassFrequencyCapFiltersForNonWinType(
filters.getKeyedFrequencyCapsForViewEvents(),
FrequencyCapFilters.AD_EVENT_TYPE_VIEW,
buyer,
currentTime)) {
return false;
}
if (!filters.getKeyedFrequencyCapsForClickEvents().isEmpty()
&& !doesAdPassFrequencyCapFiltersForNonWinType(
filters.getKeyedFrequencyCapsForClickEvents(),
FrequencyCapFilters.AD_EVENT_TYPE_CLICK,
buyer,
currentTime)) {
return false;
}
return true;
}
private boolean doesAdPassFrequencyCapFiltersForWinType(
List<KeyedFrequencyCap> keyedFrequencyCaps,
AdTechIdentifier buyer,
String customAudienceOwner,
String customAudienceName,
Instant currentTime) {
for (KeyedFrequencyCap frequencyCap : keyedFrequencyCaps) {
Instant intervalStartTime =
currentTime.minusMillis(frequencyCap.getInterval().toMillis());
int numEventsSinceStartTime =
mFrequencyCapDao.getNumEventsForCustomAudienceAfterTime(
frequencyCap.getAdCounterKey(),
buyer,
customAudienceOwner,
customAudienceName,
FrequencyCapFilters.AD_EVENT_TYPE_WIN,
intervalStartTime);
if (numEventsSinceStartTime >= frequencyCap.getMaxCount()) {
return false;
}
}
return true;
}
private boolean doesAdPassFrequencyCapFiltersForNonWinType(
List<KeyedFrequencyCap> keyedFrequencyCaps,
int adEventType,
AdTechIdentifier buyer,
Instant currentTime) {
for (KeyedFrequencyCap frequencyCap : keyedFrequencyCaps) {
Instant intervalStartTime =
currentTime.minusMillis(frequencyCap.getInterval().toMillis());
int numEventsSinceStartTime =
mFrequencyCapDao.getNumEventsForBuyerAfterTime(
frequencyCap.getAdCounterKey(), buyer, adEventType, intervalStartTime);
if (numEventsSinceStartTime >= frequencyCap.getMaxCount()) {
return false;
}
}
return true;
}
}