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);
+    }
+}