Add add filterers with only fcap or app install enabled
Bug: 331179546
Test: atest AdServicesServiceCoreUnitTests
Change-Id: I3a21e4196fda804b7b0a209707e966c20c6ea2c0
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdFilterer.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdFilterer.java
index 8ea8367..c2d0109 100644
--- a/adservices/service-core/java/com/android/adservices/service/adselection/AdFilterer.java
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdFilterer.java
@@ -23,6 +23,7 @@
import java.util.List;
/** Interface for filtering ads out of an ad selection auction. */
+// TODO(b/330840774) rename to FrequencyCapAdFilterer once we deprecate adfilterer
public interface AdFilterer {
/**
* Takes a list of CAs and returns an identical list with any ads that should be filtered
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallAdFilterer.java b/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallAdFilterer.java
new file mode 100644
index 0000000..517e499
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallAdFilterer.java
@@ -0,0 +1,47 @@
+/*
+ * 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.SignedContextualAds;
+
+import com.android.adservices.data.customaudience.DBCustomAudience;
+
+import java.util.List;
+
+/** Interface for app install filtering of ads out of an ad selection auction. */
+public interface AppInstallAdFilterer {
+ /**
+ * 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.
+ */
+ List<DBCustomAudience> filterCustomAudiences(List<DBCustomAudience> cas);
+ /**
+ * 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
+ */
+ SignedContextualAds filterContextualAds(SignedContextualAds contextualAds);
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallFiltererImpl.java b/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallFiltererImpl.java
new file mode 100644
index 0000000..757961a
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallFiltererImpl.java
@@ -0,0 +1,155 @@
+/*
+ * 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.annotation.NonNull;
+
+import com.android.adservices.LoggerFactory;
+import com.android.adservices.data.adselection.AppInstallDao;
+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 app install filters to remove ads from the selectAds auction. */
+public final class AppInstallFiltererImpl implements AppInstallAdFilterer {
+
+ private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
+ @NonNull private final Clock mClock;
+ @NonNull private final AppInstallDao mAppInstallDao;
+
+ public AppInstallFiltererImpl(@NonNull AppInstallDao appInstallDao, @NonNull Clock clock) {
+ Objects.requireNonNull(appInstallDao);
+ Objects.requireNonNull(clock);
+ mAppInstallDao = appInstallDao;
+ 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 app install 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())) {
+ 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(
+ "App install 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 app install 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())) {
+ adsList.add(ad);
+ }
+ }
+ sLogger.v(
+ "App install filtering finished. %d contextual ads of the original %d remain.",
+ adsList.size(), contextualAds.getAdsWithBid().size());
+
+ return new SignedContextualAds.Builder(contextualAds).setAdsWithBid(adsList).build();
+ } finally {
+ Tracing.endAsyncSection(Tracing.FILTERER_FILTER_CONTEXTUAL, traceCookie);
+ }
+ }
+
+ private boolean doesAdPassFilters(DBAdData ad, AdTechIdentifier buyer) {
+ if (ad.getAdFilters() == null) {
+ return true;
+ }
+ final int traceCookie = Tracing.beginAsyncSection(Tracing.FILTERER_FOR_EACH_AD);
+ boolean passes = doesAdPassAppInstallFilters(ad, buyer);
+ Tracing.endAsyncSection(Tracing.FILTERER_FOR_EACH_AD, traceCookie);
+ return passes;
+ }
+
+ 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;
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallFiltererNoOpImpl.java b/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallFiltererNoOpImpl.java
new file mode 100644
index 0000000..a17b764
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/AppInstallFiltererNoOpImpl.java
@@ -0,0 +1,58 @@
+/*
+ * 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.SignedContextualAds;
+
+import com.android.adservices.LoggerFactory;
+import com.android.adservices.data.customaudience.DBCustomAudience;
+
+import java.util.List;
+
+/** Replacement for {@link AppInstallFiltererImpl} if app install filtering is turned off. */
+public final class AppInstallFiltererNoOpImpl implements AppInstallAdFilterer {
+
+ private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
+
+ /**
+ * Identity function that returns its input.
+ *
+ * @param cas A list of CAs.
+ * @return cas
+ */
+ @Override
+ public List<DBCustomAudience> filterCustomAudiences(List<DBCustomAudience> cas) {
+ logSkip();
+ return cas;
+ }
+
+ /**
+ * Identity function that returns its input.
+ *
+ * @param contextualAds An object containing ads.
+ * @return contextual ads
+ */
+ @Override
+ public SignedContextualAds filterContextualAds(SignedContextualAds contextualAds) {
+ logSkip();
+ return contextualAds;
+ }
+
+ private static void logSkip() {
+ sLogger.v("App install filtering is disabled, skipping");
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/FrequencyCapFiltererImpl.java b/adservices/service-core/java/com/android/adservices/service/adselection/FrequencyCapFiltererImpl.java
new file mode 100644
index 0000000..b548533
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/adselection/FrequencyCapFiltererImpl.java
@@ -0,0 +1,286 @@
+/*
+ * 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.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 frequency cap filters to remove ads from the selectAds auction. */
+public final class FrequencyCapFiltererImpl implements AdFilterer {
+
+ private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
+ @NonNull private final Clock mClock;
+ @NonNull private final FrequencyCapDao mFrequencyCapDao;
+
+ public FrequencyCapFiltererImpl(
+ @NonNull FrequencyCapDao frequencyCapDao, @NonNull Clock clock) {
+ Objects.requireNonNull(frequencyCapDao);
+ Objects.requireNonNull(clock);
+ 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 frequency cap 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(
+ "Frequency cap 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 frequency cap 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(
+ "Frequency cap filtering finished. %d contextual ads of the original %d"
+ + " remain.",
+ adsList.size(), contextualAds.getAdsWithBid().size());
+
+ return new SignedContextualAds.Builder(contextualAds).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;
+ }
+ final int traceCookie = Tracing.beginAsyncSection(Tracing.FILTERER_FOR_EACH_AD);
+ boolean passes =
+ doesAdPassFrequencyCapFilters(
+ ad, buyer, customAudienceOwner, customAudienceName, currentTime);
+ Tracing.endAsyncSection(Tracing.FILTERER_FOR_EACH_AD, traceCookie);
+ return passes;
+ }
+
+ private boolean doesAdPassFrequencyCapFilters(
+ DBAdData ad,
+ AdTechIdentifier buyer,
+ String customAudienceOwner,
+ String customAudienceName,
+ Instant currentTime) {
+ if (ad.getAdFilters().getFrequencyCapFilters() == null) {
+ return true;
+ }
+ final int traceCookie = Tracing.beginAsyncSection(Tracing.FILTERER_FOR_EACH_AD);
+ try {
+
+ 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;
+ } finally {
+ Tracing.endAsyncSection(Tracing.FILTERER_FOR_EACH_AD, traceCookie);
+ }
+ }
+
+ private boolean doesAdPassFrequencyCapFiltersForWinType(
+ List<KeyedFrequencyCap> keyedFrequencyCaps,
+ AdTechIdentifier buyer,
+ String customAudienceOwner,
+ String customAudienceName,
+ Instant currentTime) {
+ final int adPassesFiltersTraceCookie =
+ Tracing.beginAsyncSection(Tracing.FILTERER_FREQUENCY_CAP_WIN);
+ try {
+ for (KeyedFrequencyCap frequencyCap : keyedFrequencyCaps) {
+ Instant intervalStartTime =
+ currentTime.minusMillis(frequencyCap.getInterval().toMillis());
+
+ final int numEventsForCATraceCookie =
+ Tracing.beginAsyncSection(Tracing.FREQUENCY_CAP_GET_NUM_EVENTS_CA);
+ int numEventsSinceStartTime =
+ mFrequencyCapDao.getNumEventsForCustomAudienceAfterTime(
+ frequencyCap.getAdCounterKey(),
+ buyer,
+ customAudienceOwner,
+ customAudienceName,
+ FrequencyCapFilters.AD_EVENT_TYPE_WIN,
+ intervalStartTime);
+ Tracing.endAsyncSection(
+ Tracing.FREQUENCY_CAP_GET_NUM_EVENTS_CA, numEventsForCATraceCookie);
+
+ if (numEventsSinceStartTime >= frequencyCap.getMaxCount()) {
+ return false;
+ }
+ }
+ return true;
+ } finally {
+ Tracing.endAsyncSection(Tracing.FILTERER_FREQUENCY_CAP_WIN, adPassesFiltersTraceCookie);
+ }
+ }
+
+ private boolean doesAdPassFrequencyCapFiltersForNonWinType(
+ List<KeyedFrequencyCap> keyedFrequencyCaps,
+ int adEventType,
+ AdTechIdentifier buyer,
+ Instant currentTime) {
+ final int adPassesFiltersTraceCookie =
+ Tracing.beginAsyncSection(Tracing.FILTERER_FREQUENCY_CAP_NON_WIN);
+ try {
+ for (KeyedFrequencyCap frequencyCap : keyedFrequencyCaps) {
+ Instant intervalStartTime =
+ currentTime.minusMillis(frequencyCap.getInterval().toMillis());
+
+ final int numEventsForBuyerTraceCookie =
+ Tracing.beginAsyncSection(Tracing.FREQUENCY_CAP_GET_NUM_EVENTS_BUYER);
+ int numEventsSinceStartTime =
+ mFrequencyCapDao.getNumEventsForBuyerAfterTime(
+ frequencyCap.getAdCounterKey(),
+ buyer,
+ adEventType,
+ intervalStartTime);
+ Tracing.endAsyncSection(
+ Tracing.FREQUENCY_CAP_GET_NUM_EVENTS_BUYER, numEventsForBuyerTraceCookie);
+
+ if (numEventsSinceStartTime >= frequencyCap.getMaxCount()) {
+ return false;
+ }
+ }
+ return true;
+ } finally {
+ Tracing.endAsyncSection(
+ Tracing.FILTERER_FREQUENCY_CAP_NON_WIN, adPassesFiltersTraceCookie);
+ }
+ }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/AppInstallFiltererImplTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/AppInstallFiltererImplTest.java
new file mode 100644
index 0000000..2df3e6c
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/AppInstallFiltererImplTest.java
@@ -0,0 +1,358 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.adservices.adselection.AdWithBid;
+import android.adservices.adselection.SignedContextualAds;
+import android.adservices.adselection.SignedContextualAdsFixture;
+import android.adservices.common.AdData;
+import android.adservices.common.AdDataFixture;
+import android.adservices.common.AdFilters;
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.AppInstallFilters;
+import android.adservices.common.CommonFixture;
+import android.util.Pair;
+
+import com.android.adservices.common.DBAdDataFixture;
+import com.android.adservices.common.SdkLevelSupportRule;
+import com.android.adservices.customaudience.DBCustomAudienceFixture;
+import com.android.adservices.data.adselection.AppInstallDao;
+import com.android.adservices.data.common.DBAdData;
+import com.android.adservices.data.customaudience.DBCustomAudience;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AppInstallFiltererImplTest {
+ private static final String PACKAGE_NAME_TO_FILTER =
+ CommonFixture.TEST_PACKAGE_NAME_1 + ".filter";
+ private static final AppInstallFilters APP_INSTALL_FILTERS_TO_FILTER =
+ new AppInstallFilters.Builder()
+ .setPackageNames(new HashSet<>(Arrays.asList(PACKAGE_NAME_TO_FILTER)))
+ .build();
+ private static final DBAdData AD_TO_FILTER_ON_APP_INSTALL =
+ DBAdDataFixture.getValidDbAdDataBuilder()
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setAppInstallFilters(APP_INSTALL_FILTERS_TO_FILTER)
+ .build())
+ .build();
+
+ private static final AdData.Builder AD_DATA_BUILDER =
+ AdDataFixture.getValidFilterAdDataBuilderByBuyer(CommonFixture.VALID_BUYER_1, 0);
+
+ private static final AdData AD_DATA = AD_DATA_BUILDER.build();
+
+ private static final AdData AD_DATA_NO_FILTER =
+ new AdData.Builder()
+ .setRenderUri(AD_DATA.getRenderUri())
+ .setMetadata(AD_DATA.getMetadata())
+ .build();
+
+ private static final SignedContextualAds.Builder CONTEXTUAL_ADS_BUILDER =
+ SignedContextualAdsFixture.aContextualAdsWithEmptySignatureBuilder()
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(AD_DATA, 1.0)))
+ .setBuyer(CommonFixture.VALID_BUYER_1)
+ .setDecisionLogicUri(
+ CommonFixture.getUri(CommonFixture.VALID_BUYER_1, "/decisionPath/"));
+
+ @Mock private AppInstallDao mAppInstallDaoMock;
+ private AppInstallAdFilterer mAdFilterer;
+
+ @Rule(order = 0)
+ public final SdkLevelSupportRule sdkLevel = SdkLevelSupportRule.forAtLeastS();
+
+ @Before
+ public void setup() {
+ mAdFilterer =
+ new AppInstallFiltererImpl(
+ mAppInstallDaoMock, CommonFixture.FIXED_CLOCK_TRUNCATED_TO_MILLI);
+ }
+
+ @Test
+ public void testFilterContextualAdsDoesNotFilterNullAdFilters() {
+ final AdData adData = AD_DATA_BUILDER.setAdFilters(null).build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ }
+
+ @Test
+ public void testFilterContextualAdsDoesNotFilterNullComponentFilters() {
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setAppInstallFilters(null)
+ .setFrequencyCapFilters(null)
+ .build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ verifyNoMoreInteractions(mAppInstallDaoMock);
+ }
+
+ @Test
+ public void testFilterContextualAdsDoesNotFilterForAppNotInstalled() {
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), any())).thenReturn(false);
+ AppInstallFilters appFilters =
+ new AppInstallFilters.Builder()
+ .setPackageNames(Collections.singleton(CommonFixture.TEST_PACKAGE_NAME_1))
+ .build();
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters).build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+
+ verify(mAppInstallDaoMock)
+ .canBuyerFilterPackage(
+ CommonFixture.VALID_BUYER_1, CommonFixture.TEST_PACKAGE_NAME_1);
+ }
+
+ @Test
+ public void testFilterContextualAdsFiltersForAppInstalled() {
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), any())).thenReturn(true);
+ AppInstallFilters appFilters =
+ new AppInstallFilters.Builder()
+ .setPackageNames(Collections.singleton(CommonFixture.TEST_PACKAGE_NAME_1))
+ .build();
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters).build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+
+ assertEquals(
+ Collections.EMPTY_LIST,
+ mAdFilterer.filterContextualAds(contextualAds).getAdsWithBid());
+ verify(mAppInstallDaoMock)
+ .canBuyerFilterPackage(
+ CommonFixture.VALID_BUYER_1, CommonFixture.TEST_PACKAGE_NAME_1);
+ }
+
+ @Test
+ public void testFilterContextualAdsFiltersForMixedApps() {
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), eq(CommonFixture.TEST_PACKAGE_NAME_1)))
+ .thenReturn(true);
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), eq(CommonFixture.TEST_PACKAGE_NAME_2)))
+ .thenReturn(false);
+ AppInstallFilters appFilters =
+ new AppInstallFilters.Builder()
+ .setPackageNames(
+ new HashSet<>(
+ Arrays.asList(
+ CommonFixture.TEST_PACKAGE_NAME_1,
+ CommonFixture.TEST_PACKAGE_NAME_2)))
+ .build();
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters).build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+
+ assertEquals(
+ Collections.EMPTY_LIST,
+ mAdFilterer.filterContextualAds(contextualAds).getAdsWithBid());
+ verify(mAppInstallDaoMock)
+ .canBuyerFilterPackage(
+ eq(CommonFixture.VALID_BUYER_1), eq(CommonFixture.TEST_PACKAGE_NAME_2));
+ }
+
+ @Test
+ public void testFilterContextualAdsFiltersForMixedAds() {
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), eq(CommonFixture.TEST_PACKAGE_NAME_1)))
+ .thenReturn(true);
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), eq(CommonFixture.TEST_PACKAGE_NAME_2)))
+ .thenReturn(false);
+ AppInstallFilters appFilters1 =
+ new AppInstallFilters.Builder()
+ .setPackageNames(
+ new HashSet<>(Arrays.asList(CommonFixture.TEST_PACKAGE_NAME_1)))
+ .build();
+ AppInstallFilters appFilters2 =
+ new AppInstallFilters.Builder()
+ .setPackageNames(
+ new HashSet<>(Arrays.asList(CommonFixture.TEST_PACKAGE_NAME_2)))
+ .build();
+ final AdData adData1 =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters1).build())
+ .build();
+ final AdData adData2 =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters2).build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(
+ ImmutableList.of(
+ new AdWithBid(adData1, 1.0), new AdWithBid(adData2, 2.0)))
+ .build();
+
+ assertEquals(
+ Arrays.asList(new AdWithBid(adData2, 2.0)),
+ mAdFilterer.filterContextualAds(contextualAds).getAdsWithBid());
+ verify(mAppInstallDaoMock)
+ .canBuyerFilterPackage(
+ eq(CommonFixture.VALID_BUYER_1), eq(CommonFixture.TEST_PACKAGE_NAME_1));
+ verify(mAppInstallDaoMock)
+ .canBuyerFilterPackage(
+ eq(CommonFixture.VALID_BUYER_1), eq(CommonFixture.TEST_PACKAGE_NAME_2));
+ }
+
+ @Test
+ public void testFilterCustomAudiencesNothingFiltered() {
+ List<DBCustomAudience> caList =
+ DBCustomAudienceFixture.getListOfBuyersCustomAudiences(
+ Arrays.asList(CommonFixture.VALID_BUYER_1, CommonFixture.VALID_BUYER_2));
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), any())).thenReturn(false);
+ assertEquals(caList, mAdFilterer.filterCustomAudiences(caList));
+ validateAppInstallDBCalls(caList);
+ }
+
+ @Test
+ public void testFilterCustomAudiencesOneAdFiltered() {
+ List<DBCustomAudience> caListWithoutFilterAd =
+ DBCustomAudienceFixture.getListOfBuyersCustomAudiences(
+ Arrays.asList(CommonFixture.VALID_BUYER_1, CommonFixture.VALID_BUYER_2));
+ List<DBCustomAudience> caListWithFilterAd =
+ DBCustomAudienceFixture.getListOfBuyersCustomAudiences(
+ Arrays.asList(CommonFixture.VALID_BUYER_1, CommonFixture.VALID_BUYER_2));
+ caListWithFilterAd.get(0).getAds().add(AD_TO_FILTER_ON_APP_INSTALL);
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), any())).thenReturn(false);
+ when(mAppInstallDaoMock.canBuyerFilterPackage(
+ CommonFixture.VALID_BUYER_1, PACKAGE_NAME_TO_FILTER))
+ .thenReturn(true);
+
+ assertEquals(caListWithoutFilterAd, mAdFilterer.filterCustomAudiences(caListWithFilterAd));
+ validateAppInstallDBCalls(caListWithFilterAd);
+ }
+
+ @Test
+ public void testFilterCustomAudiencesWholeCaFiltered() {
+ List<DBCustomAudience> caListWithoutFilteredCa =
+ DBCustomAudienceFixture.getListOfBuyersCustomAudiences(
+ Arrays.asList(CommonFixture.VALID_BUYER_2));
+ List<DBCustomAudience> caListWithFilteredCa =
+ DBCustomAudienceFixture.getListOfBuyersCustomAudiences(
+ Arrays.asList(CommonFixture.VALID_BUYER_2));
+ DBCustomAudience caToFilter =
+ DBCustomAudienceFixture.getValidBuilderByBuyer(CommonFixture.VALID_BUYER_1)
+ .setAds(Arrays.asList(AD_TO_FILTER_ON_APP_INSTALL))
+ .build();
+ caListWithFilteredCa.add(caToFilter);
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), any())).thenReturn(false);
+ when(mAppInstallDaoMock.canBuyerFilterPackage(
+ CommonFixture.VALID_BUYER_1, PACKAGE_NAME_TO_FILTER))
+ .thenReturn(true);
+
+ assertEquals(
+ caListWithoutFilteredCa, mAdFilterer.filterCustomAudiences(caListWithFilteredCa));
+ validateAppInstallDBCalls(caListWithFilteredCa);
+ }
+
+ @Test
+ public void testFilterCustomAudiencesAllCasFiltered() {
+ DBCustomAudience caToFilterOnAppInstall =
+ DBCustomAudienceFixture.getValidBuilderByBuyer(CommonFixture.VALID_BUYER_1)
+ .setAds(Arrays.asList(AD_TO_FILTER_ON_APP_INSTALL))
+ .build();
+ when(mAppInstallDaoMock.canBuyerFilterPackage(any(), any())).thenReturn(false);
+ when(mAppInstallDaoMock.canBuyerFilterPackage(
+ CommonFixture.VALID_BUYER_1, PACKAGE_NAME_TO_FILTER))
+ .thenReturn(true);
+
+ assertEquals(
+ Collections.EMPTY_LIST,
+ mAdFilterer.filterCustomAudiences(Arrays.asList(caToFilterOnAppInstall)));
+ validateAppInstallDBCalls(Arrays.asList(caToFilterOnAppInstall));
+ }
+
+ private void validateAppInstallDBCalls(List<DBCustomAudience> caList) {
+ /* We want to validate that all the calls that should have been made were made, but we
+ * can't just use a captor and compare lists since we can't guarantee the order.
+ */
+ Map<Pair<AdTechIdentifier, String>, Integer> calls = new HashMap<>();
+ for (DBCustomAudience ca : caList) {
+ for (DBAdData ad : ca.getAds()) {
+ AdFilters filters = ad.getAdFilters();
+ if (filters == null || filters.getAppInstallFilters() == null) {
+ continue;
+ }
+ if (filters.getAppInstallFilters()
+ .getPackageNames()
+ .contains(PACKAGE_NAME_TO_FILTER)) {
+ calls.merge(new Pair<>(ca.getBuyer(), PACKAGE_NAME_TO_FILTER), 1, Integer::sum);
+ continue;
+ }
+ for (String packageName : filters.getAppInstallFilters().getPackageNames()) {
+ calls.merge(new Pair<>(ca.getBuyer(), packageName), 1, Integer::sum);
+ }
+ }
+ }
+ for (Pair<AdTechIdentifier, String> call : calls.keySet()) {
+ verify(mAppInstallDaoMock, times(calls.get(call)))
+ .canBuyerFilterPackage(call.first, call.second);
+ }
+ }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/AppInstallFiltererNoOpImplTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/AppInstallFiltererNoOpImplTest.java
new file mode 100644
index 0000000..975d472
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/AppInstallFiltererNoOpImplTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+import android.adservices.adselection.AdWithBid;
+import android.adservices.adselection.SignedContextualAds;
+import android.adservices.adselection.SignedContextualAdsFixture;
+import android.adservices.common.AdData;
+import android.adservices.common.AdDataFixture;
+import android.adservices.common.AdFilters;
+import android.adservices.common.AppInstallFilters;
+import android.adservices.common.CommonFixture;
+
+import com.android.adservices.common.SdkLevelSupportRule;
+import com.android.adservices.customaudience.DBCustomAudienceFixture;
+import com.android.adservices.data.customaudience.DBCustomAudience;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AppInstallFiltererNoOpImplTest {
+ private static final AdData.Builder AD_DATA_BUILDER =
+ AdDataFixture.getValidFilterAdDataBuilderByBuyer(CommonFixture.VALID_BUYER_1, 0);
+
+ private static final SignedContextualAds.Builder CONTEXTUAL_ADS_BUILDER =
+ SignedContextualAdsFixture.aContextualAdsWithEmptySignatureBuilder()
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(AD_DATA_BUILDER.build(), 1.0)))
+ .setBuyer(CommonFixture.VALID_BUYER_1)
+ .setDecisionLogicUri(
+ CommonFixture.getUri(CommonFixture.VALID_BUYER_1, "/decisionPath/"));
+
+ private AppInstallAdFilterer mAdFilterer;
+
+ @Rule(order = 0)
+ public final SdkLevelSupportRule sdkLevel = SdkLevelSupportRule.forAtLeastS();
+
+ @Before
+ public void setup() {
+ mAdFilterer = new AppInstallFiltererNoOpImpl();
+ }
+
+ @Test
+ public void testFilterNullAdFilters() {
+ final AdData adData = AD_DATA_BUILDER.setAdFilters(null).build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ }
+
+ @Test
+ public void testFilterNullComponentFilters() {
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setAppInstallFilters(null)
+ .setFrequencyCapFilters(null)
+ .build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ }
+
+ @Test
+ public void testAppInstallFilter() {
+ AppInstallFilters appFilters =
+ new AppInstallFilters.Builder()
+ .setPackageNames(Collections.singleton(CommonFixture.TEST_PACKAGE_NAME_1))
+ .build();
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters).build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ }
+
+ @Test
+ public void testMultipleApps() {
+ AppInstallFilters appFilters =
+ new AppInstallFilters.Builder()
+ .setPackageNames(
+ new HashSet<>(
+ Arrays.asList(
+ CommonFixture.TEST_PACKAGE_NAME_1,
+ CommonFixture.TEST_PACKAGE_NAME_2)))
+ .build();
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters).build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ }
+
+ @Test
+ public void testMultipleAds() {
+ AppInstallFilters appFilters1 =
+ new AppInstallFilters.Builder()
+ .setPackageNames(
+ new HashSet<>(Arrays.asList(CommonFixture.TEST_PACKAGE_NAME_1)))
+ .build();
+ AppInstallFilters appFilters2 =
+ new AppInstallFilters.Builder()
+ .setPackageNames(
+ new HashSet<>(Arrays.asList(CommonFixture.TEST_PACKAGE_NAME_2)))
+ .build();
+ final AdData adData1 =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters1).build())
+ .build();
+ final AdData adData2 =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder().setAppInstallFilters(appFilters2).build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(
+ ImmutableList.of(
+ new AdWithBid(adData1, 1.0), new AdWithBid(adData2, 2.0)))
+ .build();
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ }
+
+ @Test
+ public void testFilterOnCustomAudience() {
+ List<DBCustomAudience> caList =
+ DBCustomAudienceFixture.getListOfBuyersCustomAudiences(
+ Arrays.asList(CommonFixture.VALID_BUYER_1, CommonFixture.VALID_BUYER_2));
+ assertEquals(caList, mAdFilterer.filterCustomAudiences(caList));
+ }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/FrequencyCapFiltererImplTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/FrequencyCapFiltererImplTest.java
new file mode 100644
index 0000000..7b66a0a
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/FrequencyCapFiltererImplTest.java
@@ -0,0 +1,475 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.adservices.adselection.AdWithBid;
+import android.adservices.adselection.SignedContextualAds;
+import android.adservices.adselection.SignedContextualAdsFixture;
+import android.adservices.common.AdData;
+import android.adservices.common.AdDataFixture;
+import android.adservices.common.AdFilters;
+import android.adservices.common.CommonFixture;
+import android.adservices.common.FrequencyCapFilters;
+import android.adservices.common.FrequencyCapFiltersFixture;
+import android.adservices.common.KeyedFrequencyCapFixture;
+
+import com.android.adservices.common.DBAdDataFixture;
+import com.android.adservices.common.SdkLevelSupportRule;
+import com.android.adservices.customaudience.DBCustomAudienceFixture;
+import com.android.adservices.data.adselection.FrequencyCapDao;
+import com.android.adservices.data.common.DBAdData;
+import com.android.adservices.data.customaudience.DBCustomAudience;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(MockitoJUnitRunner.class)
+public class FrequencyCapFiltererImplTest {
+ private static final String PACKAGE_NAME_TO_FILTER =
+ CommonFixture.TEST_PACKAGE_NAME_1 + ".filter";
+
+ private static final AdData.Builder AD_DATA_BUILDER =
+ AdDataFixture.getValidFilterAdDataBuilderByBuyer(CommonFixture.VALID_BUYER_1, 0);
+
+ private static final AdData AD_DATA = AD_DATA_BUILDER.build();
+
+ private static final AdData AD_DATA_NO_FILTER =
+ new AdData.Builder()
+ .setRenderUri(AD_DATA.getRenderUri())
+ .setMetadata(AD_DATA.getMetadata())
+ .build();
+
+ private static final SignedContextualAds.Builder CONTEXTUAL_ADS_BUILDER =
+ SignedContextualAdsFixture.aContextualAdsWithEmptySignatureBuilder()
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(AD_DATA, 1.0)))
+ .setBuyer(CommonFixture.VALID_BUYER_1)
+ .setDecisionLogicUri(
+ CommonFixture.getUri(CommonFixture.VALID_BUYER_1, "/decisionPath/"));
+
+ @Mock private FrequencyCapDao mFrequencyCapDaoMock;
+ private AdFilterer mAdFilterer;
+
+ @Rule(order = 0)
+ public final SdkLevelSupportRule sdkLevel = SdkLevelSupportRule.forAtLeastS();
+
+ @Before
+ public void setup() {
+ mAdFilterer =
+ new FrequencyCapFiltererImpl(
+ mFrequencyCapDaoMock, CommonFixture.FIXED_CLOCK_TRUNCATED_TO_MILLI);
+ }
+
+ @Test
+ public void testFilterContextualAdsDoesNotFilterNullAdFilters() {
+ final AdData adData = AD_DATA_BUILDER.setAdFilters(null).build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ }
+
+ @Test
+ public void testFilterContextualAdsDoesNotFilterNullComponentFilters() {
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setAppInstallFilters(null)
+ .setFrequencyCapFilters(null)
+ .build())
+ .build();
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER
+ .setAdsWithBid(ImmutableList.of(new AdWithBid(adData, 1.0)))
+ .build();
+
+ assertEquals(contextualAds, mAdFilterer.filterContextualAds(contextualAds));
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+
+ @Test
+ public void testFilterContextualAdsWithEmptyFrequencyCapFilters() {
+ doReturn(KeyedFrequencyCapFixture.FILTER_EXCEED_COUNT)
+ .when(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(anyInt(), any(), anyInt(), any());
+
+ final AdData adDataNotFiltered =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ new FrequencyCapFilters.Builder().build())
+ .build())
+ .build();
+ final AdData dataNoFilters = AD_DATA_NO_FILTER;
+ List<AdWithBid> adsWithBid =
+ ImmutableList.of(
+ new AdWithBid(adDataNotFiltered, 1.0), new AdWithBid(dataNoFilters, 2.0));
+
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER.setAdsWithBid(adsWithBid).build();
+
+ assertThat(mAdFilterer.filterContextualAds(contextualAds).getAdsWithBid())
+ .containsExactlyElementsIn(adsWithBid);
+
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+
+ @Test
+ public void testFilterContextualAdsForNonMatchingFrequencyCap() {
+ doReturn(KeyedFrequencyCapFixture.FILTER_UNDER_MAX_COUNT)
+ .when(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(anyInt(), any(), anyInt(), any());
+
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ FrequencyCapFiltersFixture
+ .VALID_FREQUENCY_CAP_FILTERS)
+ .build())
+ .build();
+ final AdData dataNoFilters = AD_DATA_NO_FILTER;
+ List<AdWithBid> adsWithBid =
+ ImmutableList.of(new AdWithBid(adData, 1.0), new AdWithBid(dataNoFilters, 2.0));
+
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER.setAdsWithBid(adsWithBid).build();
+
+ assertThat(mAdFilterer.filterContextualAds(contextualAds).getAdsWithBid())
+ .containsExactlyElementsIn(adsWithBid);
+
+ verify(
+ mFrequencyCapDaoMock,
+ times(KeyedFrequencyCapFixture.VALID_KEYED_FREQUENCY_CAP_LIST.size()))
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_IMPRESSION), any());
+ verify(
+ mFrequencyCapDaoMock,
+ times(KeyedFrequencyCapFixture.VALID_KEYED_FREQUENCY_CAP_LIST.size()))
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_VIEW), any());
+ verify(
+ mFrequencyCapDaoMock,
+ times(KeyedFrequencyCapFixture.VALID_KEYED_FREQUENCY_CAP_LIST.size()))
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_CLICK), any());
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+
+ @Test
+ public void testFilterContextualAdsForMatchingFrequencyCap() {
+ doReturn(KeyedFrequencyCapFixture.FILTER_EXCEED_COUNT)
+ .when(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(anyInt(), any(), anyInt(), any());
+
+ final AdData adData =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ FrequencyCapFiltersFixture
+ .VALID_FREQUENCY_CAP_FILTERS)
+ .build())
+ .build();
+ final AdData dataNoFilters = AD_DATA_NO_FILTER;
+ List<AdWithBid> adsWithBid =
+ ImmutableList.of(new AdWithBid(adData, 1.0), new AdWithBid(dataNoFilters, 2.0));
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER.setAdsWithBid(adsWithBid).build();
+
+ assertThat(mAdFilterer.filterContextualAds(contextualAds).getAdsWithBid())
+ .containsExactly(new AdWithBid(dataNoFilters, 2.0));
+ verify(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(anyInt(), any(), anyInt(), any());
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+
+ @Test
+ public void testFilterContextualAdsDoNotFilterWinFrequencyCaps() {
+ final AdData adDataOnlyWinFilters =
+ AD_DATA_BUILDER
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ FrequencyCapFiltersFixture
+ .VALID_FREQUENCY_CAP_FILTERS_ONLY_WIN)
+ .build())
+ .build();
+ final AdData dataNoFilters = AD_DATA_NO_FILTER;
+ List<AdWithBid> adsWithBid =
+ ImmutableList.of(
+ new AdWithBid(adDataOnlyWinFilters, 1.0),
+ new AdWithBid(dataNoFilters, 2.0));
+ final SignedContextualAds contextualAds =
+ CONTEXTUAL_ADS_BUILDER.setAdsWithBid(adsWithBid).build();
+
+ assertThat(mAdFilterer.filterContextualAds(contextualAds).getAdsWithBid())
+ .containsExactlyElementsIn(adsWithBid);
+
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+
+ @Test
+ public void testFilterCustomAudiencesNothingFiltered() {
+ List<DBCustomAudience> caList =
+ DBCustomAudienceFixture.getListOfBuyersCustomAudiences(
+ Arrays.asList(CommonFixture.VALID_BUYER_1, CommonFixture.VALID_BUYER_2));
+ assertEquals(caList, mAdFilterer.filterCustomAudiences(caList));
+ }
+
+ @Test
+ public void testFilterCustomAudiencesWithEmptyFrequencyCapFilters() {
+ DBCustomAudience caWithEmptyFrequencyCapFilters =
+ DBCustomAudienceFixture.getValidBuilderByBuyerNoFilters(CommonFixture.VALID_BUYER_1)
+ .setAds(
+ Arrays.asList(
+ DBAdDataFixture.getValidDbAdDataNoFiltersBuilder()
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ new FrequencyCapFilters
+ .Builder()
+ .build())
+ .build())
+ .build(),
+ DBAdDataFixture.VALID_DB_AD_DATA_NO_FILTERS))
+ .build();
+
+ List<DBCustomAudience> inputList =
+ Arrays.asList(
+ caWithEmptyFrequencyCapFilters,
+ DBCustomAudienceFixture.VALID_DB_CUSTOM_AUDIENCE_NO_FILTERS);
+
+ assertThat(mAdFilterer.filterCustomAudiences(inputList))
+ .containsExactlyElementsIn(inputList);
+
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+
+ @Test
+ public void testFilterCustomAudiencesWithNonMatchingFrequencyCapFilters() {
+ doReturn(KeyedFrequencyCapFixture.FILTER_UNDER_MAX_COUNT)
+ .when(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(anyInt(), any(), anyInt(), any());
+
+ DBAdData adDataNotFiltered =
+ DBAdDataFixture.getValidDbAdDataNoFiltersBuilder()
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ FrequencyCapFiltersFixture
+ .VALID_FREQUENCY_CAP_FILTERS)
+ .build())
+ .build();
+
+ DBCustomAudience caWithEmptyFrequencyCapFilters =
+ DBCustomAudienceFixture.getValidBuilderByBuyerNoFilters(CommonFixture.VALID_BUYER_1)
+ .setAds(
+ Arrays.asList(
+ adDataNotFiltered,
+ DBAdDataFixture.VALID_DB_AD_DATA_NO_FILTERS))
+ .build();
+
+ List<DBCustomAudience> inputList =
+ Arrays.asList(
+ caWithEmptyFrequencyCapFilters,
+ DBCustomAudienceFixture.VALID_DB_CUSTOM_AUDIENCE_NO_FILTERS);
+
+ assertThat(mAdFilterer.filterCustomAudiences(inputList))
+ .containsExactlyElementsIn(inputList);
+
+ verify(
+ mFrequencyCapDaoMock,
+ times(KeyedFrequencyCapFixture.VALID_KEYED_FREQUENCY_CAP_LIST.size()))
+ .getNumEventsForCustomAudienceAfterTime(
+ anyInt(),
+ any(),
+ any(),
+ any(),
+ eq(FrequencyCapFilters.AD_EVENT_TYPE_WIN),
+ any());
+ verify(
+ mFrequencyCapDaoMock,
+ times(KeyedFrequencyCapFixture.VALID_KEYED_FREQUENCY_CAP_LIST.size()))
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_IMPRESSION), any());
+ verify(
+ mFrequencyCapDaoMock,
+ times(KeyedFrequencyCapFixture.VALID_KEYED_FREQUENCY_CAP_LIST.size()))
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_VIEW), any());
+ verify(
+ mFrequencyCapDaoMock,
+ times(KeyedFrequencyCapFixture.VALID_KEYED_FREQUENCY_CAP_LIST.size()))
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_CLICK), any());
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+
+ @Test
+ public void testFilterCustomAudiencesWithMatchingNonWinFrequencyCapFilters() {
+ doReturn(KeyedFrequencyCapFixture.FILTER_EXCEED_COUNT)
+ .when(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(anyInt(), any(), anyInt(), any());
+
+ DBAdData adDataWithImpressionFilter =
+ DBAdDataFixture.getValidDbAdDataNoFiltersBuilder()
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ FrequencyCapFiltersFixture
+ .VALID_FREQUENCY_CAP_FILTERS_ONLY_IMPRESSION)
+ .build())
+ .build();
+
+ DBAdData adDataWithViewFilter =
+ DBAdDataFixture.getValidDbAdDataNoFiltersBuilder()
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ FrequencyCapFiltersFixture
+ .VALID_FREQUENCY_CAP_FILTERS_ONLY_VIEW)
+ .build())
+ .build();
+
+ DBAdData adDataWithClickFilter =
+ DBAdDataFixture.getValidDbAdDataNoFiltersBuilder()
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ FrequencyCapFiltersFixture
+ .VALID_FREQUENCY_CAP_FILTERS_ONLY_CLICK)
+ .build())
+ .build();
+
+ DBCustomAudience caWithNonWinFrequencyCapFilters =
+ DBCustomAudienceFixture.getValidBuilderByBuyerNoFilters(CommonFixture.VALID_BUYER_1)
+ .setAds(
+ Arrays.asList(
+ adDataWithImpressionFilter,
+ adDataWithViewFilter,
+ adDataWithClickFilter,
+ DBAdDataFixture.VALID_DB_AD_DATA_NO_FILTERS))
+ .build();
+
+ DBCustomAudience caWithEmptyFrequencyCapFilters =
+ DBCustomAudienceFixture.getValidBuilderByBuyerNoFilters(CommonFixture.VALID_BUYER_1)
+ .setAds(Arrays.asList(DBAdDataFixture.VALID_DB_AD_DATA_NO_FILTERS))
+ .build();
+
+ List<DBCustomAudience> inputList =
+ Arrays.asList(
+ caWithNonWinFrequencyCapFilters,
+ DBCustomAudienceFixture.VALID_DB_CUSTOM_AUDIENCE_NO_FILTERS);
+
+ assertThat(mAdFilterer.filterCustomAudiences(inputList))
+ .containsExactly(
+ caWithEmptyFrequencyCapFilters,
+ DBCustomAudienceFixture.VALID_DB_CUSTOM_AUDIENCE_NO_FILTERS);
+
+ verify(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_IMPRESSION), any());
+ verify(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_VIEW), any());
+ verify(mFrequencyCapDaoMock)
+ .getNumEventsForBuyerAfterTime(
+ anyInt(), any(), eq(FrequencyCapFilters.AD_EVENT_TYPE_CLICK), any());
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+
+ @Test
+ public void testFilterCustomAudiencesWithMatchingWinFrequencyCapFilters() {
+ doReturn(KeyedFrequencyCapFixture.FILTER_EXCEED_COUNT)
+ .when(mFrequencyCapDaoMock)
+ .getNumEventsForCustomAudienceAfterTime(
+ anyInt(),
+ any(),
+ any(),
+ any(),
+ eq(FrequencyCapFilters.AD_EVENT_TYPE_WIN),
+ any());
+
+ DBAdData adDataWithWinFilter =
+ DBAdDataFixture.getValidDbAdDataNoFiltersBuilder()
+ .setAdFilters(
+ new AdFilters.Builder()
+ .setFrequencyCapFilters(
+ FrequencyCapFiltersFixture
+ .VALID_FREQUENCY_CAP_FILTERS_ONLY_WIN)
+ .build())
+ .build();
+
+ DBCustomAudience caWithWinFrequencyCapFilters =
+ DBCustomAudienceFixture.getValidBuilderByBuyerNoFilters(CommonFixture.VALID_BUYER_1)
+ .setAds(
+ Arrays.asList(
+ adDataWithWinFilter,
+ DBAdDataFixture.VALID_DB_AD_DATA_NO_FILTERS))
+ .build();
+
+ DBCustomAudience caWithEmptyFrequencyCapFilters =
+ DBCustomAudienceFixture.getValidBuilderByBuyerNoFilters(CommonFixture.VALID_BUYER_1)
+ .setAds(Arrays.asList(DBAdDataFixture.VALID_DB_AD_DATA_NO_FILTERS))
+ .build();
+
+ List<DBCustomAudience> inputList =
+ Arrays.asList(
+ caWithWinFrequencyCapFilters,
+ DBCustomAudienceFixture.VALID_DB_CUSTOM_AUDIENCE_NO_FILTERS);
+
+ assertThat(mAdFilterer.filterCustomAudiences(inputList))
+ .containsExactly(
+ caWithEmptyFrequencyCapFilters,
+ DBCustomAudienceFixture.VALID_DB_CUSTOM_AUDIENCE_NO_FILTERS);
+
+ verify(mFrequencyCapDaoMock)
+ .getNumEventsForCustomAudienceAfterTime(
+ anyInt(),
+ any(),
+ any(),
+ any(),
+ eq(FrequencyCapFilters.AD_EVENT_TYPE_WIN),
+ any());
+ verifyNoMoreInteractions(mFrequencyCapDaoMock);
+ }
+}