From d3691c0cef6951d3c88e9cdb33c7760719d5f542 Mon Sep 17 00:00:00 2001 From: Shen Guo Date: Mon, 16 Dec 2024 13:15:58 -0800 Subject: [PATCH] Filling more logic to create a custom audience Summary: ## Context GPS Protected Audience API is an on-device technology for ads retargeting purpose. It allows ad tech to join users into custom audience groups based on their app events, and shows more personalized ads to users based on the groups. We will utilize the API on our client-side ranking product. ## This diff * Created the CA name based on the app event name and app id. * Set more fields to make sure that the ca joining request can be run through. Reviewed By: youerkang Differential Revision: D67260269 fbshipit-source-id: cc6cbbe2fc21a80c1aeba005f8e4319c64109b52 --- .../android/adservices/common/AdData.java | 8 ++- .../adservices/common/AdSelectionSignals.java | 9 +++ .../customaudience/CustomAudience.java | 11 +++ .../customaudience/TrustedBiddingData.java | 32 +++++++++ .../facebook/appevents/AppEventsLoggerImpl.kt | 2 +- .../gps/pa/PACustomAudienceClient.kt | 45 ++++++++++-- .../gps/pa/PACustomAudienceClientTest.kt | 68 ++++++++++++++++--- 7 files changed, 161 insertions(+), 14 deletions(-) create mode 100644 facebook-core/src/main/java/android/adservices/common/AdSelectionSignals.java create mode 100644 facebook-core/src/main/java/android/adservices/customaudience/TrustedBiddingData.java diff --git a/facebook-core/src/main/java/android/adservices/common/AdData.java b/facebook-core/src/main/java/android/adservices/common/AdData.java index ee20a2f537..82a6430d7f 100644 --- a/facebook-core/src/main/java/android/adservices/common/AdData.java +++ b/facebook-core/src/main/java/android/adservices/common/AdData.java @@ -8,14 +8,20 @@ package android.adservices.common; +import android.net.Uri; + import androidx.annotation.NonNull; public class AdData { - public static final class Builder { + public static class Builder { public AdData.Builder setMetadata(@NonNull String metadata) { throw new RuntimeException("Stub!"); } + public AdData.Builder setRenderUri(@NonNull Uri renderUri){ + throw new RuntimeException("Stub!"); + } + public AdData build() { throw new RuntimeException("Stub!"); } diff --git a/facebook-core/src/main/java/android/adservices/common/AdSelectionSignals.java b/facebook-core/src/main/java/android/adservices/common/AdSelectionSignals.java new file mode 100644 index 0000000000..6f8fc89d86 --- /dev/null +++ b/facebook-core/src/main/java/android/adservices/common/AdSelectionSignals.java @@ -0,0 +1,9 @@ +package android.adservices.common; + +import androidx.annotation.NonNull; + +public class AdSelectionSignals { + public static AdSelectionSignals fromString(@NonNull String source) { + throw new RuntimeException("Stub!"); + } +} diff --git a/facebook-core/src/main/java/android/adservices/customaudience/CustomAudience.java b/facebook-core/src/main/java/android/adservices/customaudience/CustomAudience.java index d31922089c..2eebc02bbc 100644 --- a/facebook-core/src/main/java/android/adservices/customaudience/CustomAudience.java +++ b/facebook-core/src/main/java/android/adservices/customaudience/CustomAudience.java @@ -9,6 +9,7 @@ package android.adservices.customaudience; import android.adservices.common.AdData; +import android.adservices.common.AdSelectionSignals; import android.adservices.common.AdTechIdentifier; import android.net.Uri; @@ -42,6 +43,16 @@ public CustomAudience.Builder setAds(@Nullable List ads) { throw new RuntimeException("Stub!"); } + public CustomAudience.Builder setUserBiddingSignals( + @Nullable AdSelectionSignals userBiddingSignals) { + throw new RuntimeException("Stub!"); + } + + public CustomAudience.Builder setTrustedBiddingData( + @Nullable TrustedBiddingData trustedBiddingData) { + throw new RuntimeException("Stub!"); + } + public CustomAudience build() { throw new RuntimeException("Stub!"); } diff --git a/facebook-core/src/main/java/android/adservices/customaudience/TrustedBiddingData.java b/facebook-core/src/main/java/android/adservices/customaudience/TrustedBiddingData.java new file mode 100644 index 0000000000..ff765ce780 --- /dev/null +++ b/facebook-core/src/main/java/android/adservices/customaudience/TrustedBiddingData.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + */ + +package android.adservices.customaudience; + +import android.net.Uri; + +import androidx.annotation.NonNull; + +import java.util.List; + +public class TrustedBiddingData { + public static class Builder { + public Builder setTrustedBiddingUri(@NonNull Uri trustedBiddingUri) { + throw new RuntimeException("Stub!"); + } + + public Builder setTrustedBiddingKeys(@NonNull List trustedBiddingKeys) { + throw new RuntimeException("Stub!"); + } + + public TrustedBiddingData build() { + throw new RuntimeException("Stub!"); + } + } + +} diff --git a/facebook-core/src/main/java/com/facebook/appevents/AppEventsLoggerImpl.kt b/facebook-core/src/main/java/com/facebook/appevents/AppEventsLoggerImpl.kt index edf8137615..6aba64cdd5 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/AppEventsLoggerImpl.kt +++ b/facebook-core/src/main/java/com/facebook/appevents/AppEventsLoggerImpl.kt @@ -688,7 +688,7 @@ internal constructor(activityName: String, applicationId: String?, accessToken: GpsAraTriggersManager.registerTriggerAsync(accessTokenAppId.applicationId, event) } if (isEnabled(FeatureManager.Feature.GPSPACAProcessing)) { - PACustomAudienceClient.joinCustomAudience() + PACustomAudienceClient.joinCustomAudience(accessTokenAppId.applicationId, event) } // Make sure Activated_App is always before other app events diff --git a/facebook-core/src/main/java/com/facebook/appevents/gps/pa/PACustomAudienceClient.kt b/facebook-core/src/main/java/com/facebook/appevents/gps/pa/PACustomAudienceClient.kt index 5aa469bf11..09740bd87f 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/gps/pa/PACustomAudienceClient.kt +++ b/facebook-core/src/main/java/com/facebook/appevents/gps/pa/PACustomAudienceClient.kt @@ -9,15 +9,21 @@ package com.facebook.appevents.gps.pa +import android.adservices.common.AdData +import android.adservices.common.AdSelectionSignals import android.adservices.common.AdTechIdentifier import android.adservices.customaudience.CustomAudience import android.adservices.customaudience.CustomAudienceManager import android.adservices.customaudience.JoinCustomAudienceRequest +import android.adservices.customaudience.TrustedBiddingData import android.annotation.TargetApi import android.net.Uri import android.os.OutcomeReceiver import android.util.Log import com.facebook.FacebookSdk +import com.facebook.appevents.AppEvent +import com.facebook.appevents.internal.Constants +import com.facebook.appevents.restrictivedatafilter.RestrictiveDataManager import com.facebook.internal.instrument.crashshield.AutoHandleExceptions import java.util.concurrent.Executors @@ -25,6 +31,10 @@ import java.util.concurrent.Executors object PACustomAudienceClient { private val TAG = "Fledge: " + PACustomAudienceClient::class.java.simpleName private const val BUYER = "facebook.com" + private const val BASE_URI = "https://www.facebook.com/privacy_sandbox/pa/logic" + private const val DELIMITER = "@" + // Sync with RestrictiveDataManager.REPLACEMENT_STRING + private const val REPLACEMENT_STRING = "_removed_" private var enabled = false private var customAudienceManager: CustomAudienceManager? = null @@ -47,7 +57,7 @@ object PACustomAudienceClient { } @TargetApi(34) - fun joinCustomAudience() { + fun joinCustomAudience(appId: String, event: AppEvent) { if (!enabled) return val callback: OutcomeReceiver = @@ -62,12 +72,30 @@ object PACustomAudienceClient { } try { + val caName = validateAndCreateCAName(appId, event) ?: return + + // Each custom audience has to be attached with at least one ad to be valid, so we need to create a dummy ad and attach it to the ca. + val dummyAd = AdData.Builder() + .setRenderUri(Uri.parse("$BASE_URI/ad")) + .setMetadata("{'isRealAd': false}") + .build() + val trustedBiddingData = TrustedBiddingData.Builder() + .setTrustedBiddingUri(Uri.parse("$BASE_URI?trusted_bidding")) + .setTrustedBiddingKeys(listOf("")) + .build() + val ca: CustomAudience = - CustomAudience.Builder().setName("").setBuyer(AdTechIdentifier.fromString(BUYER)) - .setDailyUpdateUri(Uri.parse("")).setBiddingLogicUri(Uri.parse("")) - .build() + CustomAudience.Builder() + .setName(caName) + .setBuyer(AdTechIdentifier.fromString(BUYER)) + .setDailyUpdateUri(Uri.parse("$BASE_URI?daily")) + .setBiddingLogicUri(Uri.parse("$BASE_URI?bidding")) + .setTrustedBiddingData(trustedBiddingData) + .setUserBiddingSignals(AdSelectionSignals.fromString("{}")) + .setAds(listOf(dummyAd)).build() val request: JoinCustomAudienceRequest = JoinCustomAudienceRequest.Builder().setCustomAudience(ca).build() + customAudienceManager?.joinCustomAudience( request, Executors.newSingleThreadExecutor(), @@ -77,4 +105,13 @@ object PACustomAudienceClient { Log.w(TAG, "Failed to join Custom Audience: " + e.message) } } + + private fun validateAndCreateCAName(appId: String, event: AppEvent): String? { + val eventName = event.getJSONObject().get(Constants.EVENT_NAME_EVENT_KEY) + if (eventName == REPLACEMENT_STRING) { + return null + } + + return appId + DELIMITER + eventName + } } diff --git a/facebook-core/src/test/kotlin/com/facebook/appevents/gps/pa/PACustomAudienceClientTest.kt b/facebook-core/src/test/kotlin/com/facebook/appevents/gps/pa/PACustomAudienceClientTest.kt index 935037275a..d03776595c 100644 --- a/facebook-core/src/test/kotlin/com/facebook/appevents/gps/pa/PACustomAudienceClientTest.kt +++ b/facebook-core/src/test/kotlin/com/facebook/appevents/gps/pa/PACustomAudienceClientTest.kt @@ -8,15 +8,20 @@ package com.facebook.appevents.gps.pa +import android.adservices.common.AdData +import android.adservices.common.AdSelectionSignals import android.adservices.common.AdTechIdentifier import android.adservices.customaudience.CustomAudience import android.adservices.customaudience.CustomAudienceManager import android.adservices.customaudience.JoinCustomAudienceRequest +import android.adservices.customaudience.TrustedBiddingData import android.content.Context import android.net.Uri +import android.os.Bundle import android.os.OutcomeReceiver import com.facebook.FacebookPowerMockTestCase import com.facebook.FacebookSdk +import com.facebook.appevents.AppEvent import org.junit.Before import org.junit.Test import org.mockito.kotlin.any @@ -34,10 +39,9 @@ import java.util.concurrent.Executor Context::class, CustomAudienceManager::class, AdTechIdentifier::class, - CustomAudience.Builder::class, CustomAudience::class, - JoinCustomAudienceRequest.Builder::class, JoinCustomAudienceRequest::class, + AdSelectionSignals::class, PACustomAudienceClient::class ) class PACustomAudienceClientTest : FacebookPowerMockTestCase() { @@ -55,12 +59,33 @@ class PACustomAudienceClientTest : FacebookPowerMockTestCase() { mockStatic(AdTechIdentifier::class.java) whenever(AdTechIdentifier.fromString(any())).thenReturn(adTech) + val ad = mock(AdData::class.java) + val adBuilder = mock(AdData.Builder::class.java) + whenever(adBuilder.setMetadata(any())).thenReturn(adBuilder) + whenever(adBuilder.setRenderUri(any())).thenReturn(adBuilder) + whenever(adBuilder.build()).thenReturn(ad) + whenNew(AdData.Builder::class.java).withAnyArguments().thenReturn(adBuilder) + + val trustedBiddingData = mock(TrustedBiddingData::class.java) + val trustedBiddingDataBuilder = mock(TrustedBiddingData.Builder::class.java) + whenever(trustedBiddingDataBuilder.setTrustedBiddingUri(any())).thenReturn(trustedBiddingDataBuilder) + whenever(trustedBiddingDataBuilder.setTrustedBiddingKeys(any>())).thenReturn(trustedBiddingDataBuilder) + whenever(trustedBiddingDataBuilder.build()).thenReturn(trustedBiddingData) + whenNew(TrustedBiddingData.Builder::class.java).withAnyArguments().thenReturn(trustedBiddingDataBuilder) + + val adSelectionSignals = mock(AdSelectionSignals::class.java) + mockStatic(AdSelectionSignals::class.java) + whenever(AdSelectionSignals.fromString(any())).thenReturn(adSelectionSignals) + val ca = mock(CustomAudience::class.java) val caBuilder = mock(CustomAudience.Builder::class.java) whenever(caBuilder.setName(any())).thenReturn(caBuilder) whenever(caBuilder.setBuyer(any())).thenReturn(caBuilder) whenever(caBuilder.setDailyUpdateUri(any())).thenReturn(caBuilder) whenever(caBuilder.setBiddingLogicUri(any())).thenReturn(caBuilder) + whenever(caBuilder.setAds(any>())).thenReturn(caBuilder) + whenever(caBuilder.setTrustedBiddingData(any())).thenReturn(caBuilder) + whenever(caBuilder.setUserBiddingSignals(any())).thenReturn(caBuilder) whenever(caBuilder.build()).thenReturn(ca) whenNew(CustomAudience.Builder::class.java).withAnyArguments().thenReturn(caBuilder) @@ -78,11 +103,13 @@ class PACustomAudienceClientTest : FacebookPowerMockTestCase() { whenever(CustomAudienceManager.get(any())).thenReturn(null) PACustomAudienceClient.enable() - PACustomAudienceClient.joinCustomAudience() + PACustomAudienceClient.joinCustomAudience("1234", createEvent("test_event")) - verify(customAudienceManager, times(0))?.joinCustomAudience(any(), + verify(customAudienceManager, times(0))?.joinCustomAudience( + any(), any(), - any>()) + any>() + ) } @Test @@ -91,10 +118,35 @@ class PACustomAudienceClientTest : FacebookPowerMockTestCase() { whenever(CustomAudienceManager.get(any())).thenReturn(customAudienceManager) PACustomAudienceClient.enable() - PACustomAudienceClient.joinCustomAudience() - verify(customAudienceManager, times(1))?.joinCustomAudience(any(), + PACustomAudienceClient.joinCustomAudience("1234", createEvent("test_event")) + verify(customAudienceManager, times(1))?.joinCustomAudience( + any(), any(), - any>()) + any>() + ) + } + + @Test + fun testInvalidCAName() { + mockStatic(CustomAudienceManager::class.java) + whenever(CustomAudienceManager.get(any())).thenReturn(customAudienceManager) + + PACustomAudienceClient.enable() + PACustomAudienceClient.joinCustomAudience("1234", createEvent("_removed_")) + verify(customAudienceManager, times(0))?.joinCustomAudience( + any(), + any(), + any>() + ) + } + + private fun createEvent(eventName: String): AppEvent { + val params = Bundle() + return AppEvent( + "context_name", eventName, 0.0, params, false, + isInBackground = false, + currentSessionId = null + ) } } \ No newline at end of file