From 54498acc21a71f18df6ff27f60915f50bf5bb064 Mon Sep 17 00:00:00 2001 From: Max Albright Date: Fri, 4 Oct 2024 18:06:13 -0700 Subject: [PATCH] Log implicit purchase and autolog status in events Summary: We want to log an app's implicit purchase status and autolog status in events to help with IAP diagnostic purposes. These parameters let us know if an app has autologging turned on. Reviewed By: ryantobinmeta Differential Revision: D63733239 fbshipit-source-id: fc14a0097a9b7c39f6ffe704c61b6307ee964953 --- .../facebook/appevents/AppEventsLoggerImpl.kt | 1197 +++++++++-------- .../facebook/appevents/internal/Constants.kt | 7 + .../appevents/AppEventsLoggerImplTest.kt | 868 ++++++------ 3 files changed, 1118 insertions(+), 954 deletions(-) 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 fe5f6a2ea4..44e8084765 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/AppEventsLoggerImpl.kt +++ b/facebook-core/src/main/java/com/facebook/appevents/AppEventsLoggerImpl.kt @@ -19,6 +19,7 @@ import com.facebook.AccessToken.Companion.getCurrentAccessToken import com.facebook.FacebookException import com.facebook.FacebookSdk import com.facebook.LoggingBehavior +import com.facebook.UserSettingsManager import com.facebook.appevents.AppEventQueue.add import com.facebook.appevents.AppEventQueue.flush import com.facebook.appevents.AppEventQueue.getKeySet @@ -63,600 +64,666 @@ import java.util.concurrent.TimeUnit internal class AppEventsLoggerImpl internal constructor(activityName: String, applicationId: String?, accessToken: AccessToken?) { - internal constructor( - context: Context?, - applicationId: String?, - accessToken: AccessToken? - ) : this(getActivityName(context), applicationId, accessToken) - - // Instance member variables - private val contextName: String - private var accessTokenAppId: AccessTokenAppIdPair - - fun logEvent(eventName: String?) { - logEvent(eventName, null) - } - - fun logEvent(eventName: String?, parameters: Bundle? = null) { - logEvent(eventName, null, parameters, false, getCurrentSessionGuid()) - } - - fun logEvent(eventName: String?, valueToSum: Double) { - logEvent(eventName, valueToSum, null) - } - - fun logEvent(eventName: String?, valueToSum: Double, parameters: Bundle?) { - logEvent(eventName, valueToSum, parameters, false, getCurrentSessionGuid()) - } - - fun logEventFromSE(eventName: String?, buttonText: String?) { - val parameters = Bundle() - parameters.putString("_is_suggested_event", "1") - parameters.putString("_button_text", buttonText) - logEvent(eventName, parameters) - } - - fun logPurchase(purchaseAmount: BigDecimal?, currency: Currency?) { - logPurchase(purchaseAmount, currency, null) - } - - fun logPurchase(purchaseAmount: BigDecimal?, currency: Currency?, parameters: Bundle? = null) { - if (isImplicitPurchaseLoggingEnabled()) { - Log.w( - TAG, - "You are logging purchase events while auto-logging of in-app purchase is " + - "enabled in the SDK. Make sure you don't log duplicate events") - } - logPurchase(purchaseAmount, currency, parameters, false) - } - - fun logPurchaseImplicitly(purchaseAmount: BigDecimal?, currency: Currency?, parameters: Bundle?) { - logPurchase(purchaseAmount, currency, parameters, true) - } - - fun logPurchase( - purchaseAmount: BigDecimal?, - currency: Currency?, - parameters: Bundle?, - isImplicitlyLogged: Boolean - ) { - var parameters = parameters - if (purchaseAmount == null) { - notifyDeveloperError("purchaseAmount cannot be null") - return - } else if (currency == null) { - notifyDeveloperError("currency cannot be null") - return - } - if (parameters == null) { - parameters = Bundle() - } - parameters.putString(AppEventsConstants.EVENT_PARAM_CURRENCY, currency.currencyCode) - logEvent( - AppEventsConstants.EVENT_NAME_PURCHASED, - purchaseAmount.toDouble(), - parameters, - isImplicitlyLogged, - getCurrentSessionGuid()) - eagerFlush() - } - - fun logPushNotificationOpen(payload: Bundle, action: String?) { - var campaignId: String? = null - try { - val payloadString = payload.getString(PUSH_PAYLOAD_KEY) - if (isNullOrEmpty(payloadString)) { - return // Ignore the payload if no fb push payload is present. - } - val facebookPayload = JSONObject(payloadString) - campaignId = facebookPayload.getString(PUSH_PAYLOAD_CAMPAIGN_KEY) - } catch (je: JSONException) { - // ignore - } - if (campaignId == null) { - log( - LoggingBehavior.DEVELOPER_ERRORS, - TAG, - "Malformed payload specified for logging a push notification open.") - return - } - val parameters = Bundle() - parameters.putString(APP_EVENT_PUSH_PARAMETER_CAMPAIGN, campaignId) - if (action != null) { - parameters.putString(APP_EVENT_PUSH_PARAMETER_ACTION, action) - } - logEvent(APP_EVENT_NAME_PUSH_OPENED, parameters) - } - - fun logProductItem( - itemID: String?, - availability: AppEventsLogger.ProductAvailability?, - condition: AppEventsLogger.ProductCondition?, - description: String?, - imageLink: String?, - link: String?, - title: String?, - priceAmount: BigDecimal?, - currency: Currency?, - gtin: String?, - mpn: String?, - brand: String?, - parameters: Bundle? - ) { - var parameters = parameters - if (itemID == null) { - notifyDeveloperError("itemID cannot be null") - return - } else if (availability == null) { - notifyDeveloperError("availability cannot be null") - return - } else if (condition == null) { - notifyDeveloperError("condition cannot be null") - return - } else if (description == null) { - notifyDeveloperError("description cannot be null") - return - } else if (imageLink == null) { - notifyDeveloperError("imageLink cannot be null") - return - } else if (link == null) { - notifyDeveloperError("link cannot be null") - return - } else if (title == null) { - notifyDeveloperError("title cannot be null") - return - } else if (priceAmount == null) { - notifyDeveloperError("priceAmount cannot be null") - return - } else if (currency == null) { - notifyDeveloperError("currency cannot be null") - return - } else if (gtin == null && mpn == null && brand == null) { - notifyDeveloperError("Either gtin, mpn or brand is required") - return - } - if (parameters == null) { - parameters = Bundle() - } - parameters.putString(Constants.EVENT_PARAM_PRODUCT_ITEM_ID, itemID) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_AVAILABILITY, availability.name) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_CONDITION, condition.name) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_DESCRIPTION, description) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_IMAGE_LINK, imageLink) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_LINK, link) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_TITLE, title) - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_PRICE_AMOUNT, - priceAmount.setScale(3, BigDecimal.ROUND_HALF_UP).toString()) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_PRICE_CURRENCY, currency.currencyCode) - if (gtin != null) { - parameters.putString(Constants.EVENT_PARAM_PRODUCT_GTIN, gtin) - } - if (mpn != null) { - parameters.putString(Constants.EVENT_PARAM_PRODUCT_MPN, mpn) - } - if (brand != null) { - parameters.putString(Constants.EVENT_PARAM_PRODUCT_BRAND, brand) - } - logEvent(AppEventsConstants.EVENT_NAME_PRODUCT_CATALOG_UPDATE, parameters) - eagerFlush() - } - - fun flush() { - flush(FlushReason.EXPLICIT) - } - - fun isValidForAccessToken(accessToken: AccessToken): Boolean { - val other = AccessTokenAppIdPair(accessToken) - return accessTokenAppId == other - } - - fun logSdkEvent(eventName: String, valueToSum: Double?, parameters: Bundle?) { - if (!eventName.startsWith(ACCOUNT_KIT_EVENT_NAME_PREFIX)) { - Log.e( - TAG, - "logSdkEvent is deprecated and only supports account kit for legacy, " + - "please use logEvent instead") - return - } - if (FacebookSdk.getAutoLogAppEventsEnabled()) { - logEvent(eventName, valueToSum, parameters, true, getCurrentSessionGuid()) - } - } - - /** - * Returns the app ID this logger was configured to log to. - * - * @return the Facebook app ID - */ - val applicationId: String - get() = accessTokenAppId.applicationId - - fun logEventImplicitly(eventName: String?, valueToSum: Double?, parameters: Bundle?) { - logEvent(eventName, valueToSum, parameters, true, getCurrentSessionGuid()) - } - - fun logEventImplicitly( - eventName: String?, - purchaseAmount: BigDecimal?, - currency: Currency?, - parameters: Bundle? - ) { - var parameters = parameters - if (purchaseAmount == null || currency == null) { - logd(TAG, "purchaseAmount and currency cannot be null") - return - } - if (parameters == null) { - parameters = Bundle() - } - parameters.putString(AppEventsConstants.EVENT_PARAM_CURRENCY, currency.currencyCode) - logEvent(eventName, purchaseAmount.toDouble(), parameters, true, getCurrentSessionGuid()) - } - - fun logEvent( - eventName: String?, - valueToSum: Double?, - parameters: Bundle?, - isImplicitlyLogged: Boolean, - currentSessionId: UUID? - ) { - if (eventName == null || eventName.isEmpty()) { - return - } + internal constructor( + context: Context?, + applicationId: String?, + accessToken: AccessToken? + ) : this(getActivityName(context), applicationId, accessToken) - // Kill events if kill-switch is enabled - if (getGateKeeperForKey(APP_EVENTS_KILLSWITCH, FacebookSdk.getApplicationId(), false)) { - log( - LoggingBehavior.APP_EVENTS, - "AppEvents", - "KillSwitch is enabled and fail to log app event: %s", - eventName) - return - } - - // return earlier if the event name is in blocklist - if (isInBlocklist(eventName)) { - return - } - - try { - if (!ProtectedModeManager.protectedModeIsApplied(parameters)) { - processFilterSensitiveParams(parameters, eventName) - } - MACARuleMatchingManager.processParameters(parameters, eventName) - processParametersForProtectedMode(parameters) - val event = - AppEvent( - contextName, - eventName, - valueToSum, - parameters, - isImplicitlyLogged, - isInBackground(), - currentSessionId) - logEvent(event, accessTokenAppId) - } catch (jsonException: JSONException) { - // If any of the above failed, just consider this an illegal event. - log( - LoggingBehavior.APP_EVENTS, - "AppEvents", - "JSON encoding for app event failed: '%s'", - jsonException.toString()) - } catch (e: FacebookException) { - // If any of the above failed, just consider this an illegal event. - log(LoggingBehavior.APP_EVENTS, "AppEvents", "Invalid app event: %s", e.toString()) - } - } - - companion object { - // Constants - private val TAG = - AppEventsLoggerImpl::class.java.canonicalName - ?: "com.facebook.appevents.AppEventsLoggerImpl" - private const val APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS = 60 * 60 * 24 - private const val PUSH_PAYLOAD_KEY = "fb_push_payload" - private const val PUSH_PAYLOAD_CAMPAIGN_KEY = "campaign" - private const val APP_EVENT_NAME_PUSH_OPENED = "fb_mobile_push_opened" - private const val APP_EVENT_PUSH_PARAMETER_CAMPAIGN = "fb_push_campaign" - private const val APP_EVENT_PUSH_PARAMETER_ACTION = "fb_push_action" - private const val ACCOUNT_KIT_EVENT_NAME_PREFIX = "fb_ak" - private var backgroundExecutor: ScheduledThreadPoolExecutor? = null - private var flushBehaviorField = AppEventsLogger.FlushBehavior.AUTO - @JvmStatic - fun getFlushBehavior(): AppEventsLogger.FlushBehavior { - synchronized(staticLock) { - return flushBehaviorField - } - } + // Instance member variables + private val contextName: String + private var accessTokenAppId: AccessTokenAppIdPair - @JvmStatic - fun setFlushBehavior(flushBehavior: AppEventsLogger.FlushBehavior) { - synchronized(staticLock) { flushBehaviorField = flushBehavior } - } - private val staticLock = Any() - private var anonymousAppDeviceGUID: String? = null - private var isActivateAppEventRequested = false - - // Log implicit push token event and flush logger immediately - private var pushNotificationsRegistrationIdField: String? = null - @JvmStatic - fun getPushNotificationsRegistrationId(): String? { - synchronized(staticLock) { - return pushNotificationsRegistrationIdField - } - } - @JvmStatic - fun setPushNotificationsRegistrationId(registrationId: String?) { - synchronized(staticLock) { - if (!stringsEqualOrEmpty(pushNotificationsRegistrationIdField, registrationId)) { - pushNotificationsRegistrationIdField = registrationId - val logger = AppEventsLoggerImpl(FacebookSdk.getApplicationContext(), null, null) - // Log implicit push token event and flush logger immediately - logger.logEvent(AppEventsConstants.EVENT_NAME_PUSH_TOKEN_OBTAINED) - if (getFlushBehavior() != AppEventsLogger.FlushBehavior.EXPLICIT_ONLY) { - logger.flush() - } - } - } - } - private const val APP_EVENT_PREFERENCES = "com.facebook.sdk.appEventPreferences" - const val APP_EVENTS_KILLSWITCH = "app_events_killswitch" - - @JvmStatic - fun activateApp(application: Application, applicationId: String?) { - var applicationId = applicationId - if (!FacebookSdk.isInitialized()) { - throw FacebookException( - "The Facebook sdk must be initialized before calling " + "activateApp") - } - AnalyticsUserIDStore.initStore() - UserDataStore.initStore() - if (applicationId == null) { - applicationId = FacebookSdk.getApplicationId() - } - - // activateApp supersedes publishInstall in the public API, so we need to explicitly invoke - // it, since the server can't reliably infer install state for all conditions of an app - // activate. - FacebookSdk.publishInstallAsync(application, applicationId) - - // Will do nothing in case AutoLogAppEventsEnabled is true, as we already started the - // tracking as part of sdkInitialize() flow - startTracking(application, applicationId) + fun logEvent(eventName: String?) { + logEvent(eventName, null) } - @JvmStatic - fun functionDEPRECATED(extraMsg: String) { - Log.w(TAG, "This function is deprecated. $extraMsg") + fun logEvent(eventName: String?, parameters: Bundle? = null) { + logEvent(eventName, null, parameters, false, getCurrentSessionGuid()) } - @JvmStatic - fun initializeLib(context: Context, applicationId: String?) { - if (!FacebookSdk.getAutoLogAppEventsEnabled()) { - return - } - val logger = AppEventsLoggerImpl(context, applicationId, null) - checkNotNull(backgroundExecutor).execute { - val params = Bundle() - val classes = - arrayOf( // internal SDK Libraries - "com.facebook.core.Core", - "com.facebook.login.Login", - "com.facebook.share.Share", - "com.facebook.places.Places", - "com.facebook.messenger.Messenger", - "com.facebook.applinks.AppLinks", - "com.facebook.marketing.Marketing", - "com.facebook.gamingservices.GamingServices", - "com.facebook.all.All", // external SDK Libraries - "com.android.billingclient.api.BillingClient", - "com.android.vending.billing.IInAppBillingService") - val keys = - arrayOf( // internal SDK Libraries - "core_lib_included", - "login_lib_included", - "share_lib_included", - "places_lib_included", - "messenger_lib_included", - "applinks_lib_included", - "marketing_lib_included", - "gamingservices_lib_included", - "all_lib_included", // external SDK Libraries - "billing_client_lib_included", - "billing_service_lib_included") - if (classes.size != keys.size) { - throw FacebookException("Number of class names and key names should match") - } - var bitmask = 0 - for (i in classes.indices) { - val className = classes[i] - val keyName = keys[i] - try { - Class.forName(className) - params.putInt(keyName, 1) - bitmask = bitmask or (1 shl i) - } catch (ignored: ClassNotFoundException) { - /* no op */ - } - } - val preferences = context.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) - val previousBitmask = preferences.getInt("kitsBitmask", 0) - if (previousBitmask != bitmask) { - preferences.edit().putInt("kitsBitmask", bitmask).apply() - logger.logEventImplicitly(AnalyticsEvents.EVENT_SDK_INITIALIZE, null, params) - } - } + fun logEvent(eventName: String?, valueToSum: Double) { + logEvent(eventName, valueToSum, null) } - @JvmStatic - fun onContextStop() { - // TODO: (v4) add onContextStop() to samples that use the logger. - persistToDisk() + + fun logEvent(eventName: String?, valueToSum: Double, parameters: Bundle?) { + logEvent(eventName, valueToSum, parameters, false, getCurrentSessionGuid()) } - // will make async connection to try retrieve data. First time we might return null - // instead of actual result. - // Should not be a problem as subsequent calls will have the data if it exists. - @JvmStatic - fun getInstallReferrer(): String? { - tryUpdateReferrerInfo( - object : InstallReferrerUtil.Callback { - // will make async connection to try retrieve data. First time we might return null - // instead of actual result. - // Should not be a problem as subsequent calls will have the data if it exists. - override fun onReceiveReferrerUrl(s: String?) { - setInstallReferrer(s) - } - }) - val ctx = FacebookSdk.getApplicationContext() - val preferences = ctx.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) - return preferences.getString("install_referrer", null) + fun logEventFromSE(eventName: String?, buttonText: String?) { + val parameters = Bundle() + parameters.putString("_is_suggested_event", "1") + parameters.putString("_button_text", buttonText) + logEvent(eventName, parameters) } - @JvmStatic - fun setInstallReferrer(referrer: String?) { - val ctx = FacebookSdk.getApplicationContext() - val preferences = ctx.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) - if (referrer != null) { - preferences.edit().putString("install_referrer", referrer).apply() - } + + fun logPurchase(purchaseAmount: BigDecimal?, currency: Currency?) { + logPurchase(purchaseAmount, currency, null) } - @JvmStatic - fun augmentWebView(webView: WebView, context: Context?) { - val parts = Build.VERSION.RELEASE.split(".").toTypedArray() - val majorRelease = if (parts.isNotEmpty()) parts[0].toInt() else 0 - val minorRelease = if (parts.size > 1) parts[1].toInt() else 0 - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || - majorRelease < 4 || - majorRelease == 4 && minorRelease <= 1) { - log( - LoggingBehavior.DEVELOPER_ERRORS, - TAG, - "augmentWebView is only available for Android SDK version >= 17 on devices " + - "running Android >= 4.2") - return - } - webView.addJavascriptInterface( - FacebookSDKJSInterface(context), "fbmq_" + FacebookSdk.getApplicationId()) + fun logPurchase(purchaseAmount: BigDecimal?, currency: Currency?, parameters: Bundle? = null) { + if (isImplicitPurchaseLoggingEnabled()) { + Log.w( + TAG, + "You are logging purchase events while auto-logging of in-app purchase is " + + "enabled in the SDK. Make sure you don't log duplicate events" + ) + } + logPurchase(purchaseAmount, currency, parameters, false) + } + + fun logPurchaseImplicitly( + purchaseAmount: BigDecimal?, + currency: Currency?, + parameters: Bundle? + ) { + logPurchase(purchaseAmount, currency, parameters, true) + } + + fun logPurchase( + purchaseAmount: BigDecimal?, + currency: Currency?, + parameters: Bundle?, + isImplicitlyLogged: Boolean + ) { + var parameters = parameters + if (purchaseAmount == null) { + notifyDeveloperError("purchaseAmount cannot be null") + return + } else if (currency == null) { + notifyDeveloperError("currency cannot be null") + return + } + if (parameters == null) { + parameters = Bundle() + } + parameters.putString(AppEventsConstants.EVENT_PARAM_CURRENCY, currency.currencyCode) + logEvent( + AppEventsConstants.EVENT_NAME_PURCHASED, + purchaseAmount.toDouble(), + parameters, + isImplicitlyLogged, + getCurrentSessionGuid() + ) + eagerFlush() + } + + fun logPushNotificationOpen(payload: Bundle, action: String?) { + var campaignId: String? = null + try { + val payloadString = payload.getString(PUSH_PAYLOAD_KEY) + if (isNullOrEmpty(payloadString)) { + return // Ignore the payload if no fb push payload is present. + } + val facebookPayload = JSONObject(payloadString) + campaignId = facebookPayload.getString(PUSH_PAYLOAD_CAMPAIGN_KEY) + } catch (je: JSONException) { + // ignore + } + if (campaignId == null) { + log( + LoggingBehavior.DEVELOPER_ERRORS, + TAG, + "Malformed payload specified for logging a push notification open." + ) + return + } + val parameters = Bundle() + parameters.putString(APP_EVENT_PUSH_PARAMETER_CAMPAIGN, campaignId) + if (action != null) { + parameters.putString(APP_EVENT_PUSH_PARAMETER_ACTION, action) + } + logEvent(APP_EVENT_NAME_PUSH_OPENED, parameters) + } + + fun logProductItem( + itemID: String?, + availability: AppEventsLogger.ProductAvailability?, + condition: AppEventsLogger.ProductCondition?, + description: String?, + imageLink: String?, + link: String?, + title: String?, + priceAmount: BigDecimal?, + currency: Currency?, + gtin: String?, + mpn: String?, + brand: String?, + parameters: Bundle? + ) { + var parameters = parameters + if (itemID == null) { + notifyDeveloperError("itemID cannot be null") + return + } else if (availability == null) { + notifyDeveloperError("availability cannot be null") + return + } else if (condition == null) { + notifyDeveloperError("condition cannot be null") + return + } else if (description == null) { + notifyDeveloperError("description cannot be null") + return + } else if (imageLink == null) { + notifyDeveloperError("imageLink cannot be null") + return + } else if (link == null) { + notifyDeveloperError("link cannot be null") + return + } else if (title == null) { + notifyDeveloperError("title cannot be null") + return + } else if (priceAmount == null) { + notifyDeveloperError("priceAmount cannot be null") + return + } else if (currency == null) { + notifyDeveloperError("currency cannot be null") + return + } else if (gtin == null && mpn == null && brand == null) { + notifyDeveloperError("Either gtin, mpn or brand is required") + return + } + if (parameters == null) { + parameters = Bundle() + } + parameters.putString(Constants.EVENT_PARAM_PRODUCT_ITEM_ID, itemID) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_AVAILABILITY, availability.name) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_CONDITION, condition.name) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_DESCRIPTION, description) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_IMAGE_LINK, imageLink) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_LINK, link) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_TITLE, title) + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_PRICE_AMOUNT, + priceAmount.setScale(3, BigDecimal.ROUND_HALF_UP).toString() + ) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_PRICE_CURRENCY, currency.currencyCode) + if (gtin != null) { + parameters.putString(Constants.EVENT_PARAM_PRODUCT_GTIN, gtin) + } + if (mpn != null) { + parameters.putString(Constants.EVENT_PARAM_PRODUCT_MPN, mpn) + } + if (brand != null) { + parameters.putString(Constants.EVENT_PARAM_PRODUCT_BRAND, brand) + } + logEvent(AppEventsConstants.EVENT_NAME_PRODUCT_CATALOG_UPDATE, parameters) + eagerFlush() } - private fun initializeTimersIfNeeded() { - synchronized(staticLock) { - if (backgroundExecutor != null) { - return - } - // Having single runner thread enforces ordered execution of tasks, - // which matters in some cases e.g. making sure user id is set before - // trying to update user properties for a given id - backgroundExecutor = ScheduledThreadPoolExecutor(1) - } - val attributionRecheckRunnable = Runnable { - val applicationIds: MutableSet = HashSet() - for (accessTokenAppId in getKeySet()) { - applicationIds.add(accessTokenAppId.applicationId) - } - for (applicationId in applicationIds) { - queryAppSettings(applicationId, true) - } - } - checkNotNull(backgroundExecutor) - .scheduleAtFixedRate( - attributionRecheckRunnable, - 0, - APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS.toLong(), - TimeUnit.SECONDS) + fun flush() { + flush(FlushReason.EXPLICIT) } - private fun logEvent(event: AppEvent, accessTokenAppId: AccessTokenAppIdPair) { - add(accessTokenAppId, event) - if (isEnabled(FeatureManager.Feature.OnDevicePostInstallEventProcessing) && - isOnDeviceProcessingEnabled()) { - sendCustomEventAsync(accessTokenAppId.applicationId, event) - } - - // Make sure Activated_App is always before other app events - if (!event.getIsImplicit() && !isActivateAppEventRequested) { - if (event.name == AppEventsConstants.EVENT_NAME_ACTIVATED_APP) { - isActivateAppEventRequested = true - } else { - log( - LoggingBehavior.APP_EVENTS, - "AppEvents", - "Warning: Please call AppEventsLogger.activateApp(...)" + - "from the long-lived activity's onResume() method" + - "before logging other app events.") - } - } + fun isValidForAccessToken(accessToken: AccessToken): Boolean { + val other = AccessTokenAppIdPair(accessToken) + return accessTokenAppId == other } - fun eagerFlush() { - if (getFlushBehavior() != AppEventsLogger.FlushBehavior.EXPLICIT_ONLY) { - flush(FlushReason.EAGER_FLUSHING_EVENT) - } + fun logSdkEvent(eventName: String, valueToSum: Double?, parameters: Bundle?) { + if (!eventName.startsWith(ACCOUNT_KIT_EVENT_NAME_PREFIX)) { + Log.e( + TAG, + "logSdkEvent is deprecated and only supports account kit for legacy, " + + "please use logEvent instead" + ) + return + } + if (FacebookSdk.getAutoLogAppEventsEnabled()) { + logEvent(eventName, valueToSum, parameters, true, getCurrentSessionGuid()) + } } /** - * Invoke this method, rather than throwing an Exception, for situations where user/server input - * might reasonably cause this to occur, and thus don't want an exception thrown at production - * time, but do want logging notification. + * Returns the app ID this logger was configured to log to. + * + * @return the Facebook app ID */ - private fun notifyDeveloperError(message: String) { - log(LoggingBehavior.DEVELOPER_ERRORS, "AppEvents", message) - } + val applicationId: String + get() = accessTokenAppId.applicationId + + fun logEventImplicitly(eventName: String?, valueToSum: Double?, parameters: Bundle?) { + logEvent(eventName, valueToSum, parameters, true, getCurrentSessionGuid()) + } + + fun logEventImplicitly( + eventName: String?, + purchaseAmount: BigDecimal?, + currency: Currency?, + parameters: Bundle? + ) { + var parameters = parameters + if (purchaseAmount == null || currency == null) { + logd(TAG, "purchaseAmount and currency cannot be null") + return + } + if (parameters == null) { + parameters = Bundle() + } + parameters.putString(AppEventsConstants.EVENT_PARAM_CURRENCY, currency.currencyCode) + logEvent(eventName, purchaseAmount.toDouble(), parameters, true, getCurrentSessionGuid()) + } + + fun logEvent( + eventName: String?, + valueToSum: Double?, + parameters: Bundle?, + isImplicitlyLogged: Boolean, + currentSessionId: UUID? + ) { + if (eventName == null || eventName.isEmpty()) { + return + } - @JvmStatic - fun getAnalyticsExecutor(): Executor { - if (backgroundExecutor == null) { - initializeTimersIfNeeded() - } - return checkNotNull(backgroundExecutor) + // Kill events if kill-switch is enabled + if (getGateKeeperForKey(APP_EVENTS_KILLSWITCH, FacebookSdk.getApplicationId(), false)) { + log( + LoggingBehavior.APP_EVENTS, + "AppEvents", + "KillSwitch is enabled and fail to log app event: %s", + eventName + ) + return + } + + // return earlier if the event name is in blocklist + if (isInBlocklist(eventName)) { + return + } + + addImplicitPurchaseParameters(parameters) + + try { + if (!ProtectedModeManager.protectedModeIsApplied(parameters)) { + processFilterSensitiveParams(parameters, eventName) + } + MACARuleMatchingManager.processParameters(parameters, eventName) + processParametersForProtectedMode(parameters) + val event = + AppEvent( + contextName, + eventName, + valueToSum, + parameters, + isImplicitlyLogged, + isInBackground(), + currentSessionId + ) + logEvent(event, accessTokenAppId) + } catch (jsonException: JSONException) { + // If any of the above failed, just consider this an illegal event. + log( + LoggingBehavior.APP_EVENTS, + "AppEvents", + "JSON encoding for app event failed: '%s'", + jsonException.toString() + ) + } catch (e: FacebookException) { + // If any of the above failed, just consider this an illegal event. + log(LoggingBehavior.APP_EVENTS, "AppEvents", "Invalid app event: %s", e.toString()) + } } - @JvmStatic - fun getAnonymousAppDeviceGUID(context: Context): String { - if (anonymousAppDeviceGUID == null) { - synchronized(staticLock) { - if (anonymousAppDeviceGUID == null) { - val preferences = - context.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) - anonymousAppDeviceGUID = preferences.getString("anonymousAppDeviceGUID", null) + companion object { + // Constants + private val TAG = + AppEventsLoggerImpl::class.java.canonicalName + ?: "com.facebook.appevents.AppEventsLoggerImpl" + private const val APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS = 60 * 60 * 24 + private const val PUSH_PAYLOAD_KEY = "fb_push_payload" + private const val PUSH_PAYLOAD_CAMPAIGN_KEY = "campaign" + private const val APP_EVENT_NAME_PUSH_OPENED = "fb_mobile_push_opened" + private const val APP_EVENT_PUSH_PARAMETER_CAMPAIGN = "fb_push_campaign" + private const val APP_EVENT_PUSH_PARAMETER_ACTION = "fb_push_action" + private const val ACCOUNT_KIT_EVENT_NAME_PREFIX = "fb_ak" + private var backgroundExecutor: ScheduledThreadPoolExecutor? = null + private var flushBehaviorField = AppEventsLogger.FlushBehavior.AUTO + + @JvmStatic + fun getFlushBehavior(): AppEventsLogger.FlushBehavior { + synchronized(staticLock) { + return flushBehaviorField + } + } + + @JvmStatic + fun setFlushBehavior(flushBehavior: AppEventsLogger.FlushBehavior) { + synchronized(staticLock) { flushBehaviorField = flushBehavior } + } + + private val staticLock = Any() + private var anonymousAppDeviceGUID: String? = null + private var isActivateAppEventRequested = false + + // Log implicit push token event and flush logger immediately + private var pushNotificationsRegistrationIdField: String? = null + + @JvmStatic + fun getPushNotificationsRegistrationId(): String? { + synchronized(staticLock) { + return pushNotificationsRegistrationIdField + } + } + + @JvmStatic + fun setPushNotificationsRegistrationId(registrationId: String?) { + synchronized(staticLock) { + if (!stringsEqualOrEmpty(pushNotificationsRegistrationIdField, registrationId)) { + pushNotificationsRegistrationIdField = registrationId + val logger = + AppEventsLoggerImpl(FacebookSdk.getApplicationContext(), null, null) + // Log implicit push token event and flush logger immediately + logger.logEvent(AppEventsConstants.EVENT_NAME_PUSH_TOKEN_OBTAINED) + if (getFlushBehavior() != AppEventsLogger.FlushBehavior.EXPLICIT_ONLY) { + logger.flush() + } + } + } + } + + private const val APP_EVENT_PREFERENCES = "com.facebook.sdk.appEventPreferences" + const val APP_EVENTS_KILLSWITCH = "app_events_killswitch" + + @JvmStatic + fun activateApp(application: Application, applicationId: String?) { + var applicationId = applicationId + if (!FacebookSdk.isInitialized()) { + throw FacebookException( + "The Facebook sdk must be initialized before calling " + "activateApp" + ) + } + AnalyticsUserIDStore.initStore() + UserDataStore.initStore() + if (applicationId == null) { + applicationId = FacebookSdk.getApplicationId() + } + + // activateApp supersedes publishInstall in the public API, so we need to explicitly invoke + // it, since the server can't reliably infer install state for all conditions of an app + // activate. + FacebookSdk.publishInstallAsync(application, applicationId) + + // Will do nothing in case AutoLogAppEventsEnabled is true, as we already started the + // tracking as part of sdkInitialize() flow + startTracking(application, applicationId) + } + + @JvmStatic + fun addImplicitPurchaseParameters(params: Bundle?) { + if (params == null) { + return + } + if (isImplicitPurchaseLoggingEnabled()) { + params.putCharSequence( + Constants.EVENT_PARAM_IS_IMPLICIT_PURCHASE_LOGGING_ENABLED, + "1" + ) + } else { + params.putCharSequence( + Constants.EVENT_PARAM_IS_IMPLICIT_PURCHASE_LOGGING_ENABLED, + "0" + ) + } + if (UserSettingsManager.getAutoLogAppEventsEnabled()) { + params.putCharSequence( + Constants.EVENT_PARAM_IS_AUTOLOG_APP_EVENTS_ENABLED, + "1" + ) + } else { + params.putCharSequence( + Constants.EVENT_PARAM_IS_AUTOLOG_APP_EVENTS_ENABLED, + "0" + ) + } + } + + @JvmStatic + fun functionDEPRECATED(extraMsg: String) { + Log.w(TAG, "This function is deprecated. $extraMsg") + } + + @JvmStatic + fun initializeLib(context: Context, applicationId: String?) { + if (!FacebookSdk.getAutoLogAppEventsEnabled()) { + return + } + val logger = AppEventsLoggerImpl(context, applicationId, null) + checkNotNull(backgroundExecutor).execute { + val params = Bundle() + val classes = + arrayOf( // internal SDK Libraries + "com.facebook.core.Core", + "com.facebook.login.Login", + "com.facebook.share.Share", + "com.facebook.places.Places", + "com.facebook.messenger.Messenger", + "com.facebook.applinks.AppLinks", + "com.facebook.marketing.Marketing", + "com.facebook.gamingservices.GamingServices", + "com.facebook.all.All", // external SDK Libraries + "com.android.billingclient.api.BillingClient", + "com.android.vending.billing.IInAppBillingService" + ) + val keys = + arrayOf( // internal SDK Libraries + "core_lib_included", + "login_lib_included", + "share_lib_included", + "places_lib_included", + "messenger_lib_included", + "applinks_lib_included", + "marketing_lib_included", + "gamingservices_lib_included", + "all_lib_included", // external SDK Libraries + "billing_client_lib_included", + "billing_service_lib_included" + ) + if (classes.size != keys.size) { + throw FacebookException("Number of class names and key names should match") + } + var bitmask = 0 + for (i in classes.indices) { + val className = classes[i] + val keyName = keys[i] + try { + Class.forName(className) + params.putInt(keyName, 1) + bitmask = bitmask or (1 shl i) + } catch (ignored: ClassNotFoundException) { + /* no op */ + } + } + val preferences = + context.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) + val previousBitmask = preferences.getInt("kitsBitmask", 0) + if (previousBitmask != bitmask) { + preferences.edit().putInt("kitsBitmask", bitmask).apply() + logger.logEventImplicitly(AnalyticsEvents.EVENT_SDK_INITIALIZE, null, params) + } + } + } + + @JvmStatic + fun onContextStop() { + // TODO: (v4) add onContextStop() to samples that use the logger. + persistToDisk() + } + + // will make async connection to try retrieve data. First time we might return null + // instead of actual result. + // Should not be a problem as subsequent calls will have the data if it exists. + @JvmStatic + fun getInstallReferrer(): String? { + tryUpdateReferrerInfo( + object : InstallReferrerUtil.Callback { + // will make async connection to try retrieve data. First time we might return null + // instead of actual result. + // Should not be a problem as subsequent calls will have the data if it exists. + override fun onReceiveReferrerUrl(s: String?) { + setInstallReferrer(s) + } + }) + val ctx = FacebookSdk.getApplicationContext() + val preferences = ctx.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) + return preferences.getString("install_referrer", null) + } + + @JvmStatic + fun setInstallReferrer(referrer: String?) { + val ctx = FacebookSdk.getApplicationContext() + val preferences = ctx.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) + if (referrer != null) { + preferences.edit().putString("install_referrer", referrer).apply() + } + } + + @JvmStatic + fun augmentWebView(webView: WebView, context: Context?) { + val parts = Build.VERSION.RELEASE.split(".").toTypedArray() + val majorRelease = if (parts.isNotEmpty()) parts[0].toInt() else 0 + val minorRelease = if (parts.size > 1) parts[1].toInt() else 0 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || + majorRelease < 4 || + majorRelease == 4 && minorRelease <= 1 + ) { + log( + LoggingBehavior.DEVELOPER_ERRORS, + TAG, + "augmentWebView is only available for Android SDK version >= 17 on devices " + + "running Android >= 4.2" + ) + return + } + webView.addJavascriptInterface( + FacebookSDKJSInterface(context), "fbmq_" + FacebookSdk.getApplicationId() + ) + } + + private fun initializeTimersIfNeeded() { + synchronized(staticLock) { + if (backgroundExecutor != null) { + return + } + // Having single runner thread enforces ordered execution of tasks, + // which matters in some cases e.g. making sure user id is set before + // trying to update user properties for a given id + backgroundExecutor = ScheduledThreadPoolExecutor(1) + } + val attributionRecheckRunnable = Runnable { + val applicationIds: MutableSet = HashSet() + for (accessTokenAppId in getKeySet()) { + applicationIds.add(accessTokenAppId.applicationId) + } + for (applicationId in applicationIds) { + queryAppSettings(applicationId, true) + } + } + checkNotNull(backgroundExecutor) + .scheduleAtFixedRate( + attributionRecheckRunnable, + 0, + APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS.toLong(), + TimeUnit.SECONDS + ) + } + + private fun logEvent(event: AppEvent, accessTokenAppId: AccessTokenAppIdPair) { + add(accessTokenAppId, event) + if (isEnabled(FeatureManager.Feature.OnDevicePostInstallEventProcessing) && + isOnDeviceProcessingEnabled() + ) { + sendCustomEventAsync(accessTokenAppId.applicationId, event) + } + + // Make sure Activated_App is always before other app events + if (!event.getIsImplicit() && !isActivateAppEventRequested) { + if (event.name == AppEventsConstants.EVENT_NAME_ACTIVATED_APP) { + isActivateAppEventRequested = true + } else { + log( + LoggingBehavior.APP_EVENTS, + "AppEvents", + "Warning: Please call AppEventsLogger.activateApp(...)" + + "from the long-lived activity's onResume() method" + + "before logging other app events." + ) + } + } + } + + fun eagerFlush() { + if (getFlushBehavior() != AppEventsLogger.FlushBehavior.EXPLICIT_ONLY) { + flush(FlushReason.EAGER_FLUSHING_EVENT) + } + } + + /** + * Invoke this method, rather than throwing an Exception, for situations where user/server input + * might reasonably cause this to occur, and thus don't want an exception thrown at production + * time, but do want logging notification. + */ + private fun notifyDeveloperError(message: String) { + log(LoggingBehavior.DEVELOPER_ERRORS, "AppEvents", message) + } + + @JvmStatic + fun getAnalyticsExecutor(): Executor { + if (backgroundExecutor == null) { + initializeTimersIfNeeded() + } + return checkNotNull(backgroundExecutor) + } + + @JvmStatic + fun getAnonymousAppDeviceGUID(context: Context): String { if (anonymousAppDeviceGUID == null) { - // Arbitrarily prepend XZ to distinguish from device supplied identifiers. - anonymousAppDeviceGUID = "XZ" + UUID.randomUUID().toString() - context - .getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) - .edit() - .putString("anonymousAppDeviceGUID", anonymousAppDeviceGUID) - .apply() + synchronized(staticLock) { + if (anonymousAppDeviceGUID == null) { + val preferences = + context.getSharedPreferences( + APP_EVENT_PREFERENCES, + Context.MODE_PRIVATE + ) + anonymousAppDeviceGUID = + preferences.getString("anonymousAppDeviceGUID", null) + if (anonymousAppDeviceGUID == null) { + // Arbitrarily prepend XZ to distinguish from device supplied identifiers. + anonymousAppDeviceGUID = "XZ" + UUID.randomUUID().toString() + context + .getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) + .edit() + .putString("anonymousAppDeviceGUID", anonymousAppDeviceGUID) + .apply() + } + } + } } - } + return checkNotNull(anonymousAppDeviceGUID) } - } - return checkNotNull(anonymousAppDeviceGUID) - } - } - - init { - var applicationId = applicationId - var accessToken = accessToken - sdkInitialized() - contextName = activityName - if (accessToken == null) { - accessToken = getCurrentAccessToken() } - // If we have a session and the appId passed is null or matches the session's app ID: - if (accessToken != null && - !accessToken.isExpired && - (applicationId == null || applicationId == accessToken.applicationId)) { - accessTokenAppId = AccessTokenAppIdPair(accessToken) - } else { - // If no app ID passed, get it from the manifest: - if (applicationId == null) { - applicationId = getMetadataApplicationId(FacebookSdk.getApplicationContext()) - } - accessTokenAppId = AccessTokenAppIdPair(null, checkNotNull(applicationId)) + init { + var applicationId = applicationId + var accessToken = accessToken + sdkInitialized() + contextName = activityName + if (accessToken == null) { + accessToken = getCurrentAccessToken() + } + + // If we have a session and the appId passed is null or matches the session's app ID: + if (accessToken != null && + !accessToken.isExpired && + (applicationId == null || applicationId == accessToken.applicationId) + ) { + accessTokenAppId = AccessTokenAppIdPair(accessToken) + } else { + // If no app ID passed, get it from the manifest: + if (applicationId == null) { + applicationId = getMetadataApplicationId(FacebookSdk.getApplicationContext()) + } + accessTokenAppId = AccessTokenAppIdPair(null, checkNotNull(applicationId)) + } + initializeTimersIfNeeded() } - initializeTimersIfNeeded() - } } diff --git a/facebook-core/src/main/java/com/facebook/appevents/internal/Constants.kt b/facebook-core/src/main/java/com/facebook/appevents/internal/Constants.kt index 14561b4595..95e1f91fe1 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/internal/Constants.kt +++ b/facebook-core/src/main/java/com/facebook/appevents/internal/Constants.kt @@ -118,6 +118,13 @@ object Constants { */ const val EVENT_PARAM_PRODUCT_PRICE_CURRENCY = "fb_product_price_currency" + /** + * Parameters used to let us know if an app has autologging turned on + */ + const val EVENT_PARAM_IS_AUTOLOG_APP_EVENTS_ENABLED = "is_autolog_app_events_enabled" + const val EVENT_PARAM_IS_IMPLICIT_PURCHASE_LOGGING_ENABLED = + "is_implicit_purchase_logging_enabled" + @JvmStatic fun getDefaultAppEventsSessionTimeoutInSeconds(): Int = 60 } diff --git a/facebook-core/src/test/kotlin/com/facebook/appevents/AppEventsLoggerImplTest.kt b/facebook-core/src/test/kotlin/com/facebook/appevents/AppEventsLoggerImplTest.kt index 6935935c53..71650ed76d 100644 --- a/facebook-core/src/test/kotlin/com/facebook/appevents/AppEventsLoggerImplTest.kt +++ b/facebook-core/src/test/kotlin/com/facebook/appevents/AppEventsLoggerImplTest.kt @@ -14,6 +14,8 @@ import android.webkit.WebView import com.facebook.FacebookPowerMockTestCase import com.facebook.FacebookSdk import com.facebook.FacebookSdk.GraphRequestCreator +import com.facebook.UserSettingsManager +import com.facebook.appevents.AppEventsLoggerImpl.Companion.addImplicitPurchaseParameters import com.facebook.appevents.internal.AppEventUtility import com.facebook.appevents.internal.AutomaticAnalyticsLogger import com.facebook.appevents.internal.Constants @@ -50,423 +52,511 @@ import org.robolectric.RuntimeEnvironment AppEventUtility::class, AttributionIdentifiers::class, AutomaticAnalyticsLogger::class, + UserSettingsManager::class, FetchedAppGateKeepersManager::class, FeatureManager::class, RemoteServiceWrapper::class, - OnDeviceProcessingManager::class) + OnDeviceProcessingManager::class, + + ) class AppEventsLoggerImplTest : FacebookPowerMockTestCase() { - private val mockExecutor: Executor = FacebookSerialExecutor() - private val mockAppID = "12345" - private val mockClientSecret = "abcdefg" - private val mockEventName = "fb_mock_event" - private val mockValueToSum = 1.0 - private val mockCurrency = Currency.getInstance(Locale.US) - private val mockDecimal = BigDecimal(1.0) - private val mockAttributionID = "fb_mock_attributionID" - private val mockAdvertiserID = "fb_mock_advertiserID" - private val mockAnonID = "fb_mock_anonID" - private val APP_EVENTS_KILLSWITCH = "app_events_killswitch" - private lateinit var mockParams: Bundle - private lateinit var logger: AppEventsLoggerImpl - - @Before - fun setupTest() { - FacebookSdk.setApplicationId(mockAppID) - FacebookSdk.setClientToken(mockClientSecret) - FacebookSdk.sdkInitialize(RuntimeEnvironment.application) - FacebookSdk.setExecutor(mockExecutor) - - mockParams = Bundle() - logger = AppEventsLoggerImpl(RuntimeEnvironment.application, mockAppID, mock()) - - // Stub empty implementations to AppEventQueue to not really flush events - PowerMockito.mockStatic(AppEventQueue::class.java) - - // Disable Gatekeeper - PowerMockito.mockStatic(FetchedAppGateKeepersManager::class.java) - whenever(FetchedAppGateKeepersManager.getGateKeeperForKey(any(), any(), any())) - .thenReturn(false) - - // Enable on-device event processing - PowerMockito.mockStatic(FeatureManager::class.java) - whenever(FeatureManager.isEnabled(FeatureManager.Feature.OnDeviceEventProcessing)) - .thenReturn(true) - - // Stub mock IDs for AttributionIdentifiers - val mockIdentifiers = PowerMockito.mock(AttributionIdentifiers::class.java) - whenever(mockIdentifiers.androidAdvertiserId).thenReturn(mockAdvertiserID) - whenever(mockIdentifiers.attributionId).thenReturn(mockAttributionID) - val mockCompanion: AttributionIdentifiers.Companion = mock() - WhiteboxImpl.setInternalState(AttributionIdentifiers::class.java, "Companion", mockCompanion) - whenever(mockCompanion.getAttributionIdentifiers(any())).thenReturn(mockIdentifiers) - Whitebox.setInternalState(AppEventsLoggerImpl::class.java, "anonymousAppDeviceGUID", mockAnonID) - PowerMockito.mockStatic(AutomaticAnalyticsLogger::class.java) - whenever(AutomaticAnalyticsLogger.isImplicitPurchaseLoggingEnabled()).thenReturn(true) - - // Disable AppEventUtility.isMainThread since executor now runs in main thread - PowerMockito.mockStatic(AppEventUtility::class.java) - whenever(AppEventUtility.assertIsNotMainThread()).doAnswer {} - } - - @Test - fun testSetFlushBehavior() { - AppEventsLogger.setFlushBehavior(AppEventsLogger.FlushBehavior.AUTO) - assertThat(AppEventsLogger.getFlushBehavior()).isEqualTo(AppEventsLogger.FlushBehavior.AUTO) - AppEventsLogger.setFlushBehavior(AppEventsLogger.FlushBehavior.EXPLICIT_ONLY) - assertThat(AppEventsLogger.getFlushBehavior()) - .isEqualTo(AppEventsLogger.FlushBehavior.EXPLICIT_ONLY) - } - - @Test - fun testLogEvent() { - var appEventCapture: AppEvent? = null - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventCapture = it.arguments[1] as AppEvent - Unit + private val mockExecutor: Executor = FacebookSerialExecutor() + private val mockAppID = "12345" + private val mockClientSecret = "abcdefg" + private val mockEventName = "fb_mock_event" + private val mockValueToSum = 1.0 + private val mockCurrency = Currency.getInstance(Locale.US) + private val mockDecimal = BigDecimal(1.0) + private val mockAttributionID = "fb_mock_attributionID" + private val mockAdvertiserID = "fb_mock_advertiserID" + private val mockAnonID = "fb_mock_anonID" + private val APP_EVENTS_KILLSWITCH = "app_events_killswitch" + private lateinit var mockParams: Bundle + private lateinit var logger: AppEventsLoggerImpl + + @Before + fun setupTest() { + FacebookSdk.setApplicationId(mockAppID) + FacebookSdk.setClientToken(mockClientSecret) + FacebookSdk.sdkInitialize(RuntimeEnvironment.application) + FacebookSdk.setExecutor(mockExecutor) + + mockParams = Bundle() + logger = AppEventsLoggerImpl(RuntimeEnvironment.application, mockAppID, mock()) + + // Stub empty implementations to AppEventQueue to not really flush events + PowerMockito.mockStatic(AppEventQueue::class.java) + + // Disable Gatekeeper + PowerMockito.mockStatic(FetchedAppGateKeepersManager::class.java) + whenever(FetchedAppGateKeepersManager.getGateKeeperForKey(any(), any(), any())) + .thenReturn(false) + + // Enable on-device event processing + PowerMockito.mockStatic(FeatureManager::class.java) + whenever(FeatureManager.isEnabled(FeatureManager.Feature.OnDeviceEventProcessing)) + .thenReturn(true) + + // Stub mock IDs for AttributionIdentifiers + val mockIdentifiers = PowerMockito.mock(AttributionIdentifiers::class.java) + whenever(mockIdentifiers.androidAdvertiserId).thenReturn(mockAdvertiserID) + whenever(mockIdentifiers.attributionId).thenReturn(mockAttributionID) + val mockCompanion: AttributionIdentifiers.Companion = mock() + WhiteboxImpl.setInternalState( + AttributionIdentifiers::class.java, + "Companion", + mockCompanion + ) + whenever(mockCompanion.getAttributionIdentifiers(any())).thenReturn(mockIdentifiers) + Whitebox.setInternalState( + AppEventsLoggerImpl::class.java, + "anonymousAppDeviceGUID", + mockAnonID + ) + PowerMockito.mockStatic(AutomaticAnalyticsLogger::class.java) + whenever(AutomaticAnalyticsLogger.isImplicitPurchaseLoggingEnabled()).thenReturn(true) + + // Disable AppEventUtility.isMainThread since executor now runs in main thread + PowerMockito.mockStatic(AppEventUtility::class.java) + whenever(AppEventUtility.assertIsNotMainThread()).doAnswer {} + } + + @Test + fun testSetFlushBehavior() { + AppEventsLogger.setFlushBehavior(AppEventsLogger.FlushBehavior.AUTO) + assertThat(AppEventsLogger.getFlushBehavior()).isEqualTo(AppEventsLogger.FlushBehavior.AUTO) + AppEventsLogger.setFlushBehavior(AppEventsLogger.FlushBehavior.EXPLICIT_ONLY) + assertThat(AppEventsLogger.getFlushBehavior()) + .isEqualTo(AppEventsLogger.FlushBehavior.EXPLICIT_ONLY) + } + + @Test + fun testLogEvent() { + var appEventCapture: AppEvent? = null + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventCapture = it.arguments[1] as AppEvent + Unit + } + logger.logEvent(mockEventName) + assertThat(appEventCapture?.name).isEqualTo(mockEventName) + } + + @Test + fun testAddImplicitPurchaseParameters() { + PowerMockito.mockStatic(UserSettingsManager::class.java) + val nullParams: Bundle? = null + addImplicitPurchaseParameters(nullParams) + assertThat(nullParams).isNull() + + var params = Bundle(1) + whenever(AutomaticAnalyticsLogger.isImplicitPurchaseLoggingEnabled()).thenReturn(true) + whenever(UserSettingsManager.getAutoLogAppEventsEnabled()).thenReturn(true) + addImplicitPurchaseParameters(params) + assertThat(params.getCharSequence(Constants.EVENT_PARAM_IS_AUTOLOG_APP_EVENTS_ENABLED)).isEqualTo( + "1" + ) + assertThat(params.getCharSequence(Constants.EVENT_PARAM_IS_IMPLICIT_PURCHASE_LOGGING_ENABLED)).isEqualTo( + "1" + ) + + params = Bundle(1) + whenever(AutomaticAnalyticsLogger.isImplicitPurchaseLoggingEnabled()).thenReturn(true) + whenever(UserSettingsManager.getAutoLogAppEventsEnabled()).thenReturn(false) + addImplicitPurchaseParameters(params) + assertThat(params.getCharSequence(Constants.EVENT_PARAM_IS_AUTOLOG_APP_EVENTS_ENABLED)).isEqualTo( + "0" + ) + assertThat(params.getCharSequence(Constants.EVENT_PARAM_IS_IMPLICIT_PURCHASE_LOGGING_ENABLED)).isEqualTo( + "1" + ) + + params = Bundle(1) + whenever(AutomaticAnalyticsLogger.isImplicitPurchaseLoggingEnabled()).thenReturn(false) + whenever(UserSettingsManager.getAutoLogAppEventsEnabled()).thenReturn(true) + addImplicitPurchaseParameters(params) + assertThat(params.getCharSequence(Constants.EVENT_PARAM_IS_AUTOLOG_APP_EVENTS_ENABLED)).isEqualTo( + "1" + ) + assertThat(params.getCharSequence(Constants.EVENT_PARAM_IS_IMPLICIT_PURCHASE_LOGGING_ENABLED)).isEqualTo( + "0" + ) + + params = Bundle(1) + whenever(AutomaticAnalyticsLogger.isImplicitPurchaseLoggingEnabled()).thenReturn(false) + whenever(UserSettingsManager.getAutoLogAppEventsEnabled()).thenReturn(false) + addImplicitPurchaseParameters(params) + assertThat(params.getCharSequence(Constants.EVENT_PARAM_IS_AUTOLOG_APP_EVENTS_ENABLED)).isEqualTo( + "0" + ) + assertThat(params.getCharSequence(Constants.EVENT_PARAM_IS_IMPLICIT_PURCHASE_LOGGING_ENABLED)).isEqualTo( + "0" + ) } - logger.logEvent(mockEventName) - assertThat(appEventCapture?.name).isEqualTo(mockEventName) - } - - @Test - fun testLogPurchase() { - var appEventCapture: AppEvent? = null - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventCapture = it.arguments[1] as AppEvent - Unit + + @Test + fun testLogPurchase() { + var appEventCapture: AppEvent? = null + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventCapture = it.arguments[1] as AppEvent + Unit + } + + logger.logPurchase(BigDecimal(1.0), Currency.getInstance(Locale.US)) + val parameters = Bundle() + parameters.putString( + AppEventsConstants.EVENT_PARAM_CURRENCY, Currency.getInstance(Locale.US).currencyCode + ) + assertThat(appEventCapture?.name).isEqualTo(AppEventsConstants.EVENT_NAME_PURCHASED) + + val jsonObject = appEventCapture?.getJSONObject() + assertThat(jsonObject?.getDouble(AppEventsConstants.EVENT_PARAM_VALUE_TO_SUM)).isEqualTo(1.0) + assertJsonHasParams(jsonObject, parameters) + } + + @Test + fun testLogProductItemWithGtinMpnBrand() { + var appEventCapture: AppEvent? = null + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventCapture = it.arguments[1] as AppEvent + Unit + } + + logger.logProductItem( + "F40CEE4E-471E-45DB-8541-1526043F4B21", + AppEventsLogger.ProductAvailability.IN_STOCK, + AppEventsLogger.ProductCondition.NEW, + "description", + "https://www.sample.com", + "https://www.sample.com", + "title", + BigDecimal(1.0), + Currency.getInstance(Locale.US), + "BLUE MOUNTAIN", + "BLUE MOUNTAIN", + "PHILZ", + null + ) + val parameters = Bundle() + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_ITEM_ID, "F40CEE4E-471E-45DB-8541-1526043F4B21" + ) + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_AVAILABILITY, + AppEventsLogger.ProductAvailability.IN_STOCK.name + ) + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_CONDITION, AppEventsLogger.ProductCondition.NEW.name + ) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_DESCRIPTION, "description") + parameters.putString(Constants.EVENT_PARAM_PRODUCT_IMAGE_LINK, "https://www.sample.com") + parameters.putString(Constants.EVENT_PARAM_PRODUCT_LINK, "https://www.sample.com") + parameters.putString(Constants.EVENT_PARAM_PRODUCT_TITLE, "title") + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_PRICE_AMOUNT, + BigDecimal(1.0).setScale(3, BigDecimal.ROUND_HALF_UP).toString() + ) + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_PRICE_CURRENCY, + Currency.getInstance(Locale.US).currencyCode + ) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_GTIN, "BLUE MOUNTAIN") + parameters.putString(Constants.EVENT_PARAM_PRODUCT_MPN, "BLUE MOUNTAIN") + parameters.putString(Constants.EVENT_PARAM_PRODUCT_BRAND, "PHILZ") + + assertThat(appEventCapture?.name) + .isEqualTo(AppEventsConstants.EVENT_NAME_PRODUCT_CATALOG_UPDATE) + val jsonObject = appEventCapture?.getJSONObject() + assertJsonHasParams(jsonObject, parameters) } - logger.logPurchase(BigDecimal(1.0), Currency.getInstance(Locale.US)) - val parameters = Bundle() - parameters.putString( - AppEventsConstants.EVENT_PARAM_CURRENCY, Currency.getInstance(Locale.US).currencyCode) - assertThat(appEventCapture?.name).isEqualTo(AppEventsConstants.EVENT_NAME_PURCHASED) - - val jsonObject = appEventCapture?.getJSONObject() - assertThat(jsonObject?.getDouble(AppEventsConstants.EVENT_PARAM_VALUE_TO_SUM)).isEqualTo(1.0) - assertJsonHasParams(jsonObject, parameters) - } - - @Test - fun testLogProductItemWithGtinMpnBrand() { - var appEventCapture: AppEvent? = null - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventCapture = it.arguments[1] as AppEvent - Unit + @Test + fun testLogProductItemWithoutGtinMpnBrand() { + var appEventQueueCalledTimes = 0 + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventQueueCalledTimes++ + Unit + } + + logger.logProductItem( + "F40CEE4E-471E-45DB-8541-1526043F4B21", + AppEventsLogger.ProductAvailability.IN_STOCK, + AppEventsLogger.ProductCondition.NEW, + "description", + "https://www.sample.com", + "https://www.sample.com", + "title", + BigDecimal(1.0), + Currency.getInstance(Locale.US), + null, + null, + null, + null + ) + val parameters = Bundle() + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_ITEM_ID, "F40CEE4E-471E-45DB-8541-1526043F4B21" + ) + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_AVAILABILITY, + AppEventsLogger.ProductAvailability.IN_STOCK.name + ) + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_CONDITION, AppEventsLogger.ProductCondition.NEW.name + ) + parameters.putString(Constants.EVENT_PARAM_PRODUCT_DESCRIPTION, "description") + parameters.putString(Constants.EVENT_PARAM_PRODUCT_IMAGE_LINK, "https://www.sample.com") + parameters.putString(Constants.EVENT_PARAM_PRODUCT_LINK, "https://www.sample.com") + parameters.putString(Constants.EVENT_PARAM_PRODUCT_TITLE, "title") + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_PRICE_AMOUNT, + BigDecimal(1.0).setScale(3, BigDecimal.ROUND_HALF_UP).toString() + ) + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_PRICE_CURRENCY, + Currency.getInstance(Locale.US).currencyCode + ) + + assertThat(appEventQueueCalledTimes).isEqualTo(0) } - logger.logProductItem( - "F40CEE4E-471E-45DB-8541-1526043F4B21", - AppEventsLogger.ProductAvailability.IN_STOCK, - AppEventsLogger.ProductCondition.NEW, - "description", - "https://www.sample.com", - "https://www.sample.com", - "title", - BigDecimal(1.0), - Currency.getInstance(Locale.US), - "BLUE MOUNTAIN", - "BLUE MOUNTAIN", - "PHILZ", - null) - val parameters = Bundle() - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_ITEM_ID, "F40CEE4E-471E-45DB-8541-1526043F4B21") - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_AVAILABILITY, - AppEventsLogger.ProductAvailability.IN_STOCK.name) - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_CONDITION, AppEventsLogger.ProductCondition.NEW.name) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_DESCRIPTION, "description") - parameters.putString(Constants.EVENT_PARAM_PRODUCT_IMAGE_LINK, "https://www.sample.com") - parameters.putString(Constants.EVENT_PARAM_PRODUCT_LINK, "https://www.sample.com") - parameters.putString(Constants.EVENT_PARAM_PRODUCT_TITLE, "title") - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_PRICE_AMOUNT, - BigDecimal(1.0).setScale(3, BigDecimal.ROUND_HALF_UP).toString()) - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_PRICE_CURRENCY, Currency.getInstance(Locale.US).currencyCode) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_GTIN, "BLUE MOUNTAIN") - parameters.putString(Constants.EVENT_PARAM_PRODUCT_MPN, "BLUE MOUNTAIN") - parameters.putString(Constants.EVENT_PARAM_PRODUCT_BRAND, "PHILZ") - - assertThat(appEventCapture?.name) - .isEqualTo(AppEventsConstants.EVENT_NAME_PRODUCT_CATALOG_UPDATE) - val jsonObject = appEventCapture?.getJSONObject() - assertJsonHasParams(jsonObject, parameters) - } - - @Test - fun testLogProductItemWithoutGtinMpnBrand() { - var appEventQueueCalledTimes = 0 - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventQueueCalledTimes++ - Unit + @Test + fun testLogPushNotificationOpen() { + var appEventCapture: AppEvent? = null + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventCapture = it.arguments[1] as AppEvent + Unit + } + + val payload = Bundle() + payload.putString("fb_push_payload", "{\"campaign\" : \"testCampaign\"}") + logger.logPushNotificationOpen(payload, null) + val parameters = Bundle() + parameters.putString("fb_push_campaign", "testCampaign") + + assertThat(appEventCapture?.name).isEqualTo("fb_mobile_push_opened") + val jsonObject = appEventCapture?.getJSONObject() + assertJsonHasParams(jsonObject, parameters) } - logger.logProductItem( - "F40CEE4E-471E-45DB-8541-1526043F4B21", - AppEventsLogger.ProductAvailability.IN_STOCK, - AppEventsLogger.ProductCondition.NEW, - "description", - "https://www.sample.com", - "https://www.sample.com", - "title", - BigDecimal(1.0), - Currency.getInstance(Locale.US), - null, - null, - null, - null) - val parameters = Bundle() - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_ITEM_ID, "F40CEE4E-471E-45DB-8541-1526043F4B21") - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_AVAILABILITY, - AppEventsLogger.ProductAvailability.IN_STOCK.name) - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_CONDITION, AppEventsLogger.ProductCondition.NEW.name) - parameters.putString(Constants.EVENT_PARAM_PRODUCT_DESCRIPTION, "description") - parameters.putString(Constants.EVENT_PARAM_PRODUCT_IMAGE_LINK, "https://www.sample.com") - parameters.putString(Constants.EVENT_PARAM_PRODUCT_LINK, "https://www.sample.com") - parameters.putString(Constants.EVENT_PARAM_PRODUCT_TITLE, "title") - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_PRICE_AMOUNT, - BigDecimal(1.0).setScale(3, BigDecimal.ROUND_HALF_UP).toString()) - parameters.putString( - Constants.EVENT_PARAM_PRODUCT_PRICE_CURRENCY, Currency.getInstance(Locale.US).currencyCode) - - assertThat(appEventQueueCalledTimes).isEqualTo(0) - } - - @Test - fun testLogPushNotificationOpen() { - var appEventCapture: AppEvent? = null - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventCapture = it.arguments[1] as AppEvent - Unit + @Test + fun testLogPushNotificationOpenWithoutCampaign() { + var appEventQueueCalledTimes = 0 + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventQueueCalledTimes++ + Unit + } + + val payload = Bundle() + payload.putString("fb_push_payload", "{}") + logger.logPushNotificationOpen(payload, null) + assertThat(appEventQueueCalledTimes).isEqualTo(0) } - val payload = Bundle() - payload.putString("fb_push_payload", "{\"campaign\" : \"testCampaign\"}") - logger.logPushNotificationOpen(payload, null) - val parameters = Bundle() - parameters.putString("fb_push_campaign", "testCampaign") - - assertThat(appEventCapture?.name).isEqualTo("fb_mobile_push_opened") - val jsonObject = appEventCapture?.getJSONObject() - assertJsonHasParams(jsonObject, parameters) - } - - @Test - fun testLogPushNotificationOpenWithoutCampaign() { - var appEventQueueCalledTimes = 0 - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventQueueCalledTimes++ - Unit + @Test + fun testLogPushNotificationOpenWithAction() { + var appEventCapture: AppEvent? = null + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventCapture = it.arguments[1] as AppEvent + Unit + } + + val payload = Bundle() + payload.putString("fb_push_payload", "{\"campaign\" : \"testCampaign\"}") + logger.logPushNotificationOpen(payload, "testAction") + val parameters = Bundle() + parameters.putString("fb_push_campaign", "testCampaign") + parameters.putString("fb_push_action", "testAction") + + assertThat(appEventCapture?.name).isEqualTo("fb_mobile_push_opened") + val jsonObject = appEventCapture?.getJSONObject() + assertJsonHasParams(jsonObject, parameters) } - val payload = Bundle() - payload.putString("fb_push_payload", "{}") - logger.logPushNotificationOpen(payload, null) - assertThat(appEventQueueCalledTimes).isEqualTo(0) - } - - @Test - fun testLogPushNotificationOpenWithAction() { - var appEventCapture: AppEvent? = null - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventCapture = it.arguments[1] as AppEvent - Unit + @Test + fun testLogPushNotificationOpenWithoutPayload() { + var appEventQueueCalledTimes = 0 + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventQueueCalledTimes++ + Unit + } + + val payload = Bundle() + logger.logPushNotificationOpen(payload, null) + + assertThat(appEventQueueCalledTimes).isEqualTo(0) } - val payload = Bundle() - payload.putString("fb_push_payload", "{\"campaign\" : \"testCampaign\"}") - logger.logPushNotificationOpen(payload, "testAction") - val parameters = Bundle() - parameters.putString("fb_push_campaign", "testCampaign") - parameters.putString("fb_push_action", "testAction") - - assertThat(appEventCapture?.name).isEqualTo("fb_mobile_push_opened") - val jsonObject = appEventCapture?.getJSONObject() - assertJsonHasParams(jsonObject, parameters) - } - - @Test - fun testLogPushNotificationOpenWithoutPayload() { - var appEventQueueCalledTimes = 0 - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventQueueCalledTimes++ - Unit + @Test + fun testPublishInstall() { + FacebookSdk.setAdvertiserIDCollectionEnabled(true) + val mockGraphRequestCreator: GraphRequestCreator = mock() + FacebookSdk.setGraphRequestCreator(mockGraphRequestCreator) + val expectedEvent = "MOBILE_APP_INSTALL" + val expectedUrl = "$mockAppID/activities" + val captor = ArgumentCaptor.forClass(JSONObject::class.java) + PowerMockito.mockStatic(OnDeviceProcessingManager::class.java) + whenever(OnDeviceProcessingManager.isOnDeviceProcessingEnabled()).thenReturn(true) + var sendInstallEventTimes = 0 + whenever(OnDeviceProcessingManager.sendInstallEventAsync(eq(mockAppID), any())).thenAnswer { + sendInstallEventTimes++ + Unit + } + FacebookSdk.publishInstallAsync( + FacebookSdk.getApplicationContext(), FacebookSdk.getApplicationId() + ) + verify(mockGraphRequestCreator) + .createPostRequest(isNull(), eq(expectedUrl), captor.capture(), isNull()) + assertThat(captor.value.getString("event")).isEqualTo(expectedEvent) + assertThat(captor.value.getBoolean("advertiser_tracking_enabled")).isTrue() + assertThat(captor.value.getBoolean("application_tracking_enabled")).isTrue() + assertThat(captor.value.getString("advertiser_id")).isEqualTo(mockAdvertiserID) + assertThat(captor.value.getString("attribution")).isEqualTo(mockAttributionID) + assertThat(captor.value.getString("anon_id")).isEqualTo(mockAnonID) + + assertThat(sendInstallEventTimes).isEqualTo(1) } - val payload = Bundle() - logger.logPushNotificationOpen(payload, null) - - assertThat(appEventQueueCalledTimes).isEqualTo(0) - } - - @Test - fun testPublishInstall() { - FacebookSdk.setAdvertiserIDCollectionEnabled(true) - val mockGraphRequestCreator: GraphRequestCreator = mock() - FacebookSdk.setGraphRequestCreator(mockGraphRequestCreator) - val expectedEvent = "MOBILE_APP_INSTALL" - val expectedUrl = "$mockAppID/activities" - val captor = ArgumentCaptor.forClass(JSONObject::class.java) - PowerMockito.mockStatic(OnDeviceProcessingManager::class.java) - whenever(OnDeviceProcessingManager.isOnDeviceProcessingEnabled()).thenReturn(true) - var sendInstallEventTimes = 0 - whenever(OnDeviceProcessingManager.sendInstallEventAsync(eq(mockAppID), any())).thenAnswer { - sendInstallEventTimes++ - Unit - } - FacebookSdk.publishInstallAsync( - FacebookSdk.getApplicationContext(), FacebookSdk.getApplicationId()) - verify(mockGraphRequestCreator) - .createPostRequest(isNull(), eq(expectedUrl), captor.capture(), isNull()) - assertThat(captor.value.getString("event")).isEqualTo(expectedEvent) - assertThat(captor.value.getBoolean("advertiser_tracking_enabled")).isTrue() - assertThat(captor.value.getBoolean("application_tracking_enabled")).isTrue() - assertThat(captor.value.getString("advertiser_id")).isEqualTo(mockAdvertiserID) - assertThat(captor.value.getString("attribution")).isEqualTo(mockAttributionID) - assertThat(captor.value.getString("anon_id")).isEqualTo(mockAnonID) - - assertThat(sendInstallEventTimes).isEqualTo(1) - } - - @Test - fun testPublishInstallWithAppEventsSkillswitchEnabled() { - FacebookSdk.setAdvertiserIDCollectionEnabled(true) - val mockGraphRequestCreator: GraphRequestCreator = mock() - FacebookSdk.setGraphRequestCreator(mockGraphRequestCreator) - val expectedEvent = "MOBILE_APP_INSTALL" - val expectedUrl = "$mockAppID/activities" - val captor = ArgumentCaptor.forClass(JSONObject::class.java) - - // Should not publish install event given that app_events_killswitch is turned on - PowerMockito.mockStatic(FetchedAppGateKeepersManager::class.java) - PowerMockito.`when`( + @Test + fun testPublishInstallWithAppEventsSkillswitchEnabled() { + FacebookSdk.setAdvertiserIDCollectionEnabled(true) + val mockGraphRequestCreator: GraphRequestCreator = mock() + FacebookSdk.setGraphRequestCreator(mockGraphRequestCreator) + val expectedEvent = "MOBILE_APP_INSTALL" + val expectedUrl = "$mockAppID/activities" + val captor = ArgumentCaptor.forClass(JSONObject::class.java) + + // Should not publish install event given that app_events_killswitch is turned on + PowerMockito.mockStatic(FetchedAppGateKeepersManager::class.java) + PowerMockito.`when`( FetchedAppGateKeepersManager.getGateKeeperForKey( - eq(APP_EVENTS_KILLSWITCH), any(), any())) + eq(APP_EVENTS_KILLSWITCH), any(), any() + ) + ) .thenReturn(true) - FacebookSdk.publishInstallAsync( - FacebookSdk.getApplicationContext(), FacebookSdk.getApplicationId()) - verify(mockGraphRequestCreator, never()) + FacebookSdk.publishInstallAsync( + FacebookSdk.getApplicationContext(), FacebookSdk.getApplicationId() + ) + verify(mockGraphRequestCreator, never()) .createPostRequest(isNull(), eq(expectedUrl), captor.capture(), isNull()) - } - - @Test - fun testSetPushNotificationsRegistrationId() { - val mockNotificationId = "123" - var appEventCapture: AppEvent? = null - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventCapture = it.arguments[1] as AppEvent - Unit } - AppEventsLogger.setPushNotificationsRegistrationId(mockNotificationId) - assertThat(appEventCapture?.name).isEqualTo(AppEventsConstants.EVENT_NAME_PUSH_TOKEN_OBTAINED) - assertThat(InternalAppEventsLogger.getPushNotificationsRegistrationId()) - .isEqualTo(mockNotificationId) - } - - @Test - fun testAppEventsKillSwitchDisabled() { - var appEventQueueCalledTimes = 0 - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventQueueCalledTimes++ - Unit + @Test + fun testSetPushNotificationsRegistrationId() { + val mockNotificationId = "123" + var appEventCapture: AppEvent? = null + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventCapture = it.arguments[1] as AppEvent + Unit + } + + AppEventsLogger.setPushNotificationsRegistrationId(mockNotificationId) + assertThat(appEventCapture?.name).isEqualTo(AppEventsConstants.EVENT_NAME_PUSH_TOKEN_OBTAINED) + assertThat(InternalAppEventsLogger.getPushNotificationsRegistrationId()) + .isEqualTo(mockNotificationId) } - PowerMockito.mockStatic(FetchedAppGateKeepersManager::class.java) - PowerMockito.`when`( + + @Test + fun testAppEventsKillSwitchDisabled() { + var appEventQueueCalledTimes = 0 + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventQueueCalledTimes++ + Unit + } + PowerMockito.mockStatic(FetchedAppGateKeepersManager::class.java) + PowerMockito.`when`( FetchedAppGateKeepersManager.getGateKeeperForKey( - eq(APP_EVENTS_KILLSWITCH), any(), any())) - .thenReturn(false) - logger.logEvent(mockEventName, mockValueToSum, mockParams, true, null) - logger.logEventImplicitly(mockEventName, mockDecimal, mockCurrency, mockParams) - logger.logSdkEvent(mockEventName, mockValueToSum, mockParams) - logger.logPurchase(mockDecimal, mockCurrency, mockParams, true) - logger.logPurchaseImplicitly(mockDecimal, mockCurrency, mockParams) - logger.logPushNotificationOpen(mockParams, null) - logger.logProductItem( - "F40CEE4E-471E-45DB-8541-1526043F4B21", - AppEventsLogger.ProductAvailability.IN_STOCK, - AppEventsLogger.ProductCondition.NEW, - "description", - "https://www.sample.com", - "https://www.link.com", - "title", - mockDecimal, - mockCurrency, - "GTIN", - "MPN", - "BRAND", - mockParams) - - assertThat(appEventQueueCalledTimes).isEqualTo(5) - } - - @Test - fun testAppEventsKillSwitchEnabled() { - var appEventQueueCalledTimes = 0 - whenever(AppEventQueue.add(any(), any())).thenAnswer { - appEventQueueCalledTimes++ - Unit + eq(APP_EVENTS_KILLSWITCH), any(), any() + ) + ) + .thenReturn(false) + logger.logEvent(mockEventName, mockValueToSum, mockParams, true, null) + logger.logEventImplicitly(mockEventName, mockDecimal, mockCurrency, mockParams) + logger.logSdkEvent(mockEventName, mockValueToSum, mockParams) + logger.logPurchase(mockDecimal, mockCurrency, mockParams, true) + logger.logPurchaseImplicitly(mockDecimal, mockCurrency, mockParams) + logger.logPushNotificationOpen(mockParams, null) + logger.logProductItem( + "F40CEE4E-471E-45DB-8541-1526043F4B21", + AppEventsLogger.ProductAvailability.IN_STOCK, + AppEventsLogger.ProductCondition.NEW, + "description", + "https://www.sample.com", + "https://www.link.com", + "title", + mockDecimal, + mockCurrency, + "GTIN", + "MPN", + "BRAND", + mockParams + ) + + assertThat(appEventQueueCalledTimes).isEqualTo(5) } - PowerMockito.mockStatic(FetchedAppGateKeepersManager::class.java) - PowerMockito.`when`( + + @Test + fun testAppEventsKillSwitchEnabled() { + var appEventQueueCalledTimes = 0 + whenever(AppEventQueue.add(any(), any())).thenAnswer { + appEventQueueCalledTimes++ + Unit + } + PowerMockito.mockStatic(FetchedAppGateKeepersManager::class.java) + PowerMockito.`when`( FetchedAppGateKeepersManager.getGateKeeperForKey( - eq(APP_EVENTS_KILLSWITCH), any(), any())) - .thenReturn(true) - val logger = AppEventsLoggerImpl(RuntimeEnvironment.application, mockAppID, null) - logger.logEvent(mockEventName, mockValueToSum, mockParams, true, null) - logger.logEventImplicitly(mockEventName, mockDecimal, mockCurrency, mockParams) - logger.logSdkEvent(mockEventName, mockValueToSum, mockParams) - logger.logPurchase(mockDecimal, mockCurrency, mockParams, true) - logger.logPurchaseImplicitly(mockDecimal, mockCurrency, mockParams) - logger.logPushNotificationOpen(mockParams, null) - logger.logProductItem( - "F40CEE4E-471E-45DB-8541-1526043F4B21", - AppEventsLogger.ProductAvailability.IN_STOCK, - AppEventsLogger.ProductCondition.NEW, - "description", - "https://www.sample.com", - "https://www.link.com", - "title", - mockDecimal, - mockCurrency, - "GTIN", - "MPN", - "BRAND", - mockParams) - assertThat(appEventQueueCalledTimes).isEqualTo(0) - } - - @Test - fun `test augmentWebView will run on Android api 17+`() { - Whitebox.setInternalState(Build.VERSION::class.java, "RELEASE", "4.2.0") - val mockWebView = mock() - AppEventsLoggerImpl.augmentWebView(mockWebView, RuntimeEnvironment.application) - verify(mockWebView).addJavascriptInterface(any(), any()) - } - - @Test - fun `test augmentWebView will not run on Android api less than 17 `() { - Whitebox.setInternalState(Build.VERSION::class.java, "RELEASE", "4.0.0") - val mockWebView = mock() - AppEventsLoggerImpl.augmentWebView(mockWebView, RuntimeEnvironment.application) - verify(mockWebView, never()).addJavascriptInterface(any(), any()) - } - - companion object { - private fun assertJsonHasParams(jsonObject: JSONObject?, params: Bundle) { - assertThat(jsonObject).isNotNull - for (key in params.keySet()) { - assertThat(jsonObject?.has(key)).isTrue - assertThat(jsonObject?.get(key)).isEqualTo(params[key]) - } + eq(APP_EVENTS_KILLSWITCH), any(), any() + ) + ) + .thenReturn(true) + val logger = AppEventsLoggerImpl(RuntimeEnvironment.application, mockAppID, null) + logger.logEvent(mockEventName, mockValueToSum, mockParams, true, null) + logger.logEventImplicitly(mockEventName, mockDecimal, mockCurrency, mockParams) + logger.logSdkEvent(mockEventName, mockValueToSum, mockParams) + logger.logPurchase(mockDecimal, mockCurrency, mockParams, true) + logger.logPurchaseImplicitly(mockDecimal, mockCurrency, mockParams) + logger.logPushNotificationOpen(mockParams, null) + logger.logProductItem( + "F40CEE4E-471E-45DB-8541-1526043F4B21", + AppEventsLogger.ProductAvailability.IN_STOCK, + AppEventsLogger.ProductCondition.NEW, + "description", + "https://www.sample.com", + "https://www.link.com", + "title", + mockDecimal, + mockCurrency, + "GTIN", + "MPN", + "BRAND", + mockParams + ) + assertThat(appEventQueueCalledTimes).isEqualTo(0) + } + + @Test + fun `test augmentWebView will run on Android api 17+`() { + Whitebox.setInternalState(Build.VERSION::class.java, "RELEASE", "4.2.0") + val mockWebView = mock() + AppEventsLoggerImpl.augmentWebView(mockWebView, RuntimeEnvironment.application) + verify(mockWebView).addJavascriptInterface(any(), any()) + } + + @Test + fun `test augmentWebView will not run on Android api less than 17 `() { + Whitebox.setInternalState(Build.VERSION::class.java, "RELEASE", "4.0.0") + val mockWebView = mock() + AppEventsLoggerImpl.augmentWebView(mockWebView, RuntimeEnvironment.application) + verify(mockWebView, never()).addJavascriptInterface(any(), any()) + } + + companion object { + private fun assertJsonHasParams(jsonObject: JSONObject?, params: Bundle) { + assertThat(jsonObject).isNotNull + for (key in params.keySet()) { + assertThat(jsonObject?.has(key)).isTrue + assertThat(jsonObject?.get(key)).isEqualTo(params[key]) + } + } } - } }