Skip to content

Commit

Permalink
Filling more logic to create a custom audience
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Shen Guo authored and facebook-github-bot committed Dec 16, 2024
1 parent 0fb74d3 commit d3691c0
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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!");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,6 +43,16 @@ public CustomAudience.Builder setAds(@Nullable List<AdData> 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!");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> trustedBiddingKeys) {
throw new RuntimeException("Stub!");
}

public TrustedBiddingData build() {
throw new RuntimeException("Stub!");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,32 @@

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

@AutoHandleExceptions
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

Expand All @@ -47,7 +57,7 @@ object PACustomAudienceClient {
}

@TargetApi(34)
fun joinCustomAudience() {
fun joinCustomAudience(appId: String, event: AppEvent) {
if (!enabled) return

val callback: OutcomeReceiver<Any, Exception> =
Expand All @@ -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(),
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -55,12 +59,33 @@ class PACustomAudienceClientTest : FacebookPowerMockTestCase() {
mockStatic(AdTechIdentifier::class.java)
whenever(AdTechIdentifier.fromString(any<String>())).thenReturn(adTech)

val ad = mock(AdData::class.java)
val adBuilder = mock(AdData.Builder::class.java)
whenever(adBuilder.setMetadata(any<String>())).thenReturn(adBuilder)
whenever(adBuilder.setRenderUri(any<Uri>())).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<Uri>())).thenReturn(trustedBiddingDataBuilder)
whenever(trustedBiddingDataBuilder.setTrustedBiddingKeys(any<List<String>>())).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<String>())).thenReturn(adSelectionSignals)

val ca = mock(CustomAudience::class.java)
val caBuilder = mock(CustomAudience.Builder::class.java)
whenever(caBuilder.setName(any<String>())).thenReturn(caBuilder)
whenever(caBuilder.setBuyer(any<AdTechIdentifier>())).thenReturn(caBuilder)
whenever(caBuilder.setDailyUpdateUri(any<Uri>())).thenReturn(caBuilder)
whenever(caBuilder.setBiddingLogicUri(any<Uri>())).thenReturn(caBuilder)
whenever(caBuilder.setAds(any<List<AdData>>())).thenReturn(caBuilder)
whenever(caBuilder.setTrustedBiddingData(any<TrustedBiddingData>())).thenReturn(caBuilder)
whenever(caBuilder.setUserBiddingSignals(any<AdSelectionSignals>())).thenReturn(caBuilder)
whenever(caBuilder.build()).thenReturn(ca)
whenNew(CustomAudience.Builder::class.java).withAnyArguments().thenReturn(caBuilder)

Expand All @@ -78,11 +103,13 @@ class PACustomAudienceClientTest : FacebookPowerMockTestCase() {
whenever(CustomAudienceManager.get(any<Context>())).thenReturn(null)

PACustomAudienceClient.enable()
PACustomAudienceClient.joinCustomAudience()
PACustomAudienceClient.joinCustomAudience("1234", createEvent("test_event"))

verify(customAudienceManager, times(0))?.joinCustomAudience(any<JoinCustomAudienceRequest>(),
verify(customAudienceManager, times(0))?.joinCustomAudience(
any<JoinCustomAudienceRequest>(),
any<Executor>(),
any<OutcomeReceiver<Any, Exception>>())
any<OutcomeReceiver<Any, Exception>>()
)
}

@Test
Expand All @@ -91,10 +118,35 @@ class PACustomAudienceClientTest : FacebookPowerMockTestCase() {
whenever(CustomAudienceManager.get(any<Context>())).thenReturn(customAudienceManager)

PACustomAudienceClient.enable()
PACustomAudienceClient.joinCustomAudience()
verify(customAudienceManager, times(1))?.joinCustomAudience(any<JoinCustomAudienceRequest>(),
PACustomAudienceClient.joinCustomAudience("1234", createEvent("test_event"))
verify(customAudienceManager, times(1))?.joinCustomAudience(
any<JoinCustomAudienceRequest>(),
any<Executor>(),
any<OutcomeReceiver<Any, Exception>>())
any<OutcomeReceiver<Any, Exception>>()
)
}

@Test
fun testInvalidCAName() {
mockStatic(CustomAudienceManager::class.java)
whenever(CustomAudienceManager.get(any<Context>())).thenReturn(customAudienceManager)

PACustomAudienceClient.enable()
PACustomAudienceClient.joinCustomAudience("1234", createEvent("_removed_"))
verify(customAudienceManager, times(0))?.joinCustomAudience(
any<JoinCustomAudienceRequest>(),
any<Executor>(),
any<OutcomeReceiver<Any, Exception>>()
)
}

private fun createEvent(eventName: String): AppEvent {
val params = Bundle()
return AppEvent(
"context_name", eventName, 0.0, params, false,
isInBackground = false,
currentSessionId = null
)
}

}

0 comments on commit d3691c0

Please sign in to comment.