Skip to content

Commit

Permalink
Add support for ARA trigger registration
Browse files Browse the repository at this point in the history
Summary:
Adding capability to invoke Privacy Sandbox's Attribution Reporting
API trigger request to Android SDK.
Gated behind a new feature check.
A trigger request will be sent whenever an app event is enqueued.

Reviewed By: KylinChang

Differential Revision:
D65789881

Privacy Context Container: L1250336

fbshipit-source-id: f5cab6f39e741b5718e501f031d0bed205ba8c29
  • Loading branch information
Erkang You authored and facebook-github-bot committed Dec 3, 2024
1 parent e44a8b3 commit 142b47a
Show file tree
Hide file tree
Showing 10 changed files with 421 additions and 3 deletions.
9 changes: 9 additions & 0 deletions facebook-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,17 @@
</intent-filter>
</receiver>

<!-- Support for Google Privacy Sandbox adservices API -->
<property
android:name="android.adservices.AD_SERVICES_CONFIG"
android:resource="@xml/ad_services_config" />
<uses-library android:name="android.ext.adservices" android:required="false" />
</application>

<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>

<!-- Support for Google Privacy Sandbox adservices API -->
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" />
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" />

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.
*/

/*
* 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 android.adservices.common;

import androidx.annotation.NonNull;

public interface AdServicesOutcomeReceiver<R, E extends Throwable> {
void onResult(R result);
default void onError(@NonNull E error) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.
*/

/*
* 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 android.adservices.measurement;

import android.adservices.common.AdServicesOutcomeReceiver;
import android.content.Context;
import android.net.Uri;
import android.os.OutcomeReceiver;
import java.util.concurrent.Executor;

/**
* Interface to access the android MeasurementManager class {@link
* android.adservices.measurement.MeasurementManager}. The actual implementation is going to be
* provided at runtime on the device.
*/
public class MeasurementManager {

MeasurementManager() {
throw new RuntimeException("Stub!");
}

public static MeasurementManager get(Context context) {
throw new RuntimeException("Stub!");
}

public void registerTrigger(
Uri trigger, Executor executor, OutcomeReceiver<Object, Exception> callback) {
throw new RuntimeException("Stub!");
}

public void registerTrigger(
Uri trigger, Executor executor, AdServicesOutcomeReceiver<Object, Exception> callback) {
throw new RuntimeException("Stub!");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.facebook.appevents.AppEventQueue.add
import com.facebook.appevents.AppEventQueue.flush
import com.facebook.appevents.AppEventQueue.getKeySet
import com.facebook.appevents.AppEventQueue.persistToDisk
import com.facebook.appevents.gpsara.GpsAraTriggersManager
import com.facebook.appevents.integrity.BannedParamManager.processFilterBannedParams
import com.facebook.appevents.iap.InAppPurchase
import com.facebook.appevents.iap.InAppPurchaseDedupeConfig
Expand Down Expand Up @@ -60,8 +61,6 @@ import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import org.json.JSONException
import org.json.JSONObject
import java.math.BigDecimal
import java.time.Clock
import java.util.Calendar
import java.util.Currency
import java.util.UUID
import java.util.concurrent.Executor
Expand Down Expand Up @@ -684,6 +683,9 @@ internal constructor(activityName: String, applicationId: String?, accessToken:
) {
sendCustomEventAsync(accessTokenAppId.applicationId, event)
}
if (isEnabled(FeatureManager.Feature.GPSARATriggers)) {
GpsAraTriggersManager.registerTriggerAsync(accessTokenAppId.applicationId, event)
}

// Make sure Activated_App is always before other app events
if (!event.getIsImplicit() && !isActivateAppEventRequested) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.facebook.UserSettingsManager
import com.facebook.appevents.aam.MetadataIndexer
import com.facebook.appevents.cloudbridge.AppEventsCAPIManager
import com.facebook.appevents.eventdeactivation.EventDeactivationManager
import com.facebook.appevents.gpsara.GpsAraTriggersManager
import com.facebook.appevents.iap.InAppPurchaseManager
import com.facebook.appevents.integrity.BannedParamManager
import com.facebook.appevents.integrity.BlocklistEventsManager
Expand Down Expand Up @@ -109,6 +110,11 @@ object AppEventsManager {
AppEventsCAPIManager.enable()
}
}
checkFeature(FeatureManager.Feature.GPSARATriggers) { enabled ->
if (enabled) {
GpsAraTriggersManager.enable()
}
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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 com.facebook.appevents.gpsara

import android.adservices.common.AdServicesOutcomeReceiver
import android.adservices.measurement.MeasurementManager
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.internal.AnalyticsEvents
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.net.URLEncoder

@AutoHandleExceptions
object GpsAraTriggersManager {
private var enabled = false
private val TAG = GpsAraTriggersManager::class.java.toString()
private const val SERVER_URI = "https://www.facebook.com/privacy_sandbox/mobile/register/trigger"

@JvmStatic
fun enable() {
enabled = true
}

fun registerTriggerAsync(applicationId: String, event: AppEvent) {
FacebookSdk.getExecutor().execute {
registerTrigger(applicationId, event)
}
}

@TargetApi(34)
fun registerTrigger(applicationId: String, event: AppEvent) {
if (applicationId == null) return
if (!canRegisterTrigger()) return

val context = FacebookSdk.getApplicationContext()
var measurementManager: MeasurementManager? = null

try {
measurementManager =
context.getSystemService(MeasurementManager::class.java)
if (measurementManager == null) {
// On certain Android versions, Context.getSystemService() returns null since ARA is not yet
// merged into the public SDK. If this happens, we use the factory method to get the
// MeasurementManager instance.
measurementManager = MeasurementManager.get(context.applicationContext)
}

if (measurementManager == null) {
Log.w(TAG, "FAILURE_GET_MEASUREMENT_MANAGER")
return
}

val params = getEventParameters(event)
val appIdKey = AnalyticsEvents.PARAMETER_APP_ID
val attributionTriggerUri: Uri =
Uri.parse("$SERVER_URI?$appIdKey=$applicationId&$params")

// On Android 12 and above, MeasurementManager.registerTrigger() takes an OutcomeReceiver and the
// rest takes an AdServicesOutcomeReceiver.
if (GpsCapabilityChecker.useOutcomeReceiver()) {
val outcomeReceiver: OutcomeReceiver<Any, Exception> =
object : OutcomeReceiver<Any, Exception> {
override fun onResult(result: Any) {
Log.d(TAG, "OUTCOME_RECEIVER_TRIGGER_SUCCESS")
}

override fun onError(error: Exception) {
Log.d(TAG, "OUTCOME_RECEIVER_TRIGGER_FAILURE")
}
}

measurementManager.registerTrigger(
attributionTriggerUri, FacebookSdk.getExecutor(), outcomeReceiver
)
} else {
val adServicesOutcomeReceiver: AdServicesOutcomeReceiver<Any, Exception> =
object : AdServicesOutcomeReceiver<Any, Exception> {
override fun onResult(result: Any) {
Log.d(TAG, "AD_SERVICE_OUTCOME_RECEIVER_TRIGGER_SUCCESS")
}

override fun onError(error: Exception) {
Log.d(TAG, "AD_SERVICE_OUTCOME_RECEIVER_TRIGGER_FAILURE")
}
}

measurementManager.registerTrigger(
attributionTriggerUri, FacebookSdk.getExecutor(), adServicesOutcomeReceiver
)
}

} catch (e: Exception) {
Log.w(TAG, "FAILURE_TRIGGER_REGISTRATION_FAILED")
} catch (e: NoClassDefFoundError) {
Log.w(TAG, "FAILURE_TRIGGER_REGISTRATION_NO_CLASS_FOUND")
} catch (e: NoSuchMethodError) {
Log.w(TAG, "FAILURE_TRIGGER_REGISTRATION_NO_METHOD_FOUND")
}
}

private fun canRegisterTrigger(): Boolean {
if (!enabled) {
return false
}

try {
Class.forName("android.adservices.measurement.MeasurementManager")
return true
} catch (e: Exception) {
Log.i(TAG, "FAILURE_NO_MEASUREMENT_MANAGER_CLASS")
return false
} catch (e: NoClassDefFoundError) {
Log.i(TAG, "FAILURE_NO_MEASUREMENT_MANAGER_CLASS_DEF")
return false
}
}

private fun getEventParameters(event: AppEvent): String {
val params = event.getJSONObject()

if (params == null || params.length() == 0) {
return ""
}

return params.keys().asSequence().mapNotNull { key ->
val value = params.opt(key) ?: return@mapNotNull null
try {
val encodedKey = URLEncoder.encode(key, "UTF-8")
val encodedValue = URLEncoder.encode(value.toString(), "UTF-8")
"$encodedKey=$encodedValue"
} catch (e: Exception) {
null // Ignore invalid keys
}
}
.joinToString("&")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.facebook.appevents.gpsara

import android.os.Build

object GpsCapabilityChecker {
@JvmStatic
fun useOutcomeReceiver(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ object FeatureManager {
arrayOf("com.facebook.appevents.ondeviceprocessing.")
featureMapping[Feature.IapLogging] = arrayOf("com.facebook.appevents.iap.")
featureMapping[Feature.Monitoring] = arrayOf("com.facebook.internal.logging.monitor")
featureMapping[Feature.GPSARATriggers] = arrayOf("com.facebook.appevents.gpsara.GpsARAManager")
}

private fun getGKStatus(feature: Feature): Boolean {
Expand Down Expand Up @@ -153,7 +154,8 @@ object FeatureManager {
Feature.ChromeCustomTabsPrefetching,
Feature.Monitoring,
Feature.IgnoreAppSwitchToLoggedOut,
Feature.BypassAppSwitch -> false
Feature.BypassAppSwitch,
Feature.GPSARATriggers -> false

else -> true
}
Expand Down Expand Up @@ -206,6 +208,7 @@ object FeatureManager {
ServiceUpdateCompliance(0x00030100),
Megatron(0x00040000),
Elora(0x00050000),
GPSARATriggers(0x00060000), /* privacy sandbox - attribution reporting API*/
// Features in LoginKit
/** Essential of LoginKit */
Login(0x01000000),
Expand Down Expand Up @@ -253,6 +256,7 @@ object FeatureManager {
Monitoring -> "Monitoring"
Megatron -> "Megatron"
Elora -> "Elora"
GPSARATriggers -> "GPSARATriggers"
ServiceUpdateCompliance -> "ServiceUpdateCompliance"
Login -> "LoginKit"
ChromeCustomTabsPrefetching -> "ChromeCustomTabsPrefetching"
Expand Down
26 changes: 26 additions & 0 deletions facebook-core/src/main/res/xml/ad_services_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->

<!--
Copyright (C) 2022 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.
-->

<ad-services-config>
<!-- Attribution Reporting API -->
<attribution allowAllToAccess="true" />
</ad-services-config>
Loading

0 comments on commit 142b47a

Please sign in to comment.