From fa1ebffc9cb67b5593957091c4063437cd6f3db3 Mon Sep 17 00:00:00 2001 From: Max Albright Date: Wed, 28 Aug 2024 13:07:54 -0700 Subject: [PATCH] Create enum to handle different versions of GPBL Summary: We want to have different IAP behavior depending on which version of GPBL is used. It makes sense to use an enum to model this relationship. Reviewed By: jjiang10 Differential Revision: D60729766 fbshipit-source-id: 67afd40a0154378fec72c75da97d539d88c2fa66 --- .../appevents/iap/InAppPurchaseManager.kt | 93 ++++++++---- .../appevents/iap/InAppPurchaseUtils.kt | 123 ++++++++-------- .../appevents/iap/InAppPurchaseManagerTest.kt | 135 ++++++++++-------- 3 files changed, 206 insertions(+), 145 deletions(-) diff --git a/facebook-core/src/main/java/com/facebook/appevents/iap/InAppPurchaseManager.kt b/facebook-core/src/main/java/com/facebook/appevents/iap/InAppPurchaseManager.kt index af2f19acf1..a19c61c9fe 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/iap/InAppPurchaseManager.kt +++ b/facebook-core/src/main/java/com/facebook/appevents/iap/InAppPurchaseManager.kt @@ -10,6 +10,10 @@ package com.facebook.appevents.iap import android.content.pm.PackageManager import androidx.annotation.RestrictTo +import com.facebook.appevents.iap.InAppPurchaseUtils.BillingClientVersion.NONE +import com.facebook.appevents.iap.InAppPurchaseUtils.BillingClientVersion.V1 +import com.facebook.appevents.iap.InAppPurchaseUtils.BillingClientVersion.V2_V4 +import com.facebook.appevents.iap.InAppPurchaseUtils.BillingClientVersion.V5_Plus import com.facebook.FacebookSdk.getApplicationContext import com.facebook.internal.FeatureManager import com.facebook.internal.FeatureManager.isEnabled @@ -19,40 +23,67 @@ import java.util.concurrent.atomic.AtomicBoolean @AutoHandleExceptions @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) object InAppPurchaseManager { - private const val GOOGLE_BILLINGCLIENT_VERSION = "com.google.android.play.billingclient.version" - private val enabled = AtomicBoolean(false) + private const val GOOGLE_BILLINGCLIENT_VERSION = "com.google.android.play.billingclient.version" + private val enabled = AtomicBoolean(false) - @JvmStatic - fun enableAutoLogging() { - enabled.set(true) - startTracking() - } + @JvmStatic + fun enableAutoLogging() { + enabled.set(true) + startTracking() + } + + @JvmStatic + fun startTracking() { + if (!enabled.get()) { + return + } + // Delegate IAP logic to separate handler based on Google Play Billing Library version + when (getIAPHandler()) { + NONE -> return + V1 -> InAppPurchaseActivityLifecycleTracker.startIapLogging() + V2_V4 -> { + if (isEnabled(FeatureManager.Feature.IapLoggingLib2)) { + InAppPurchaseAutoLogger.startIapLogging(getApplicationContext()) + } else { + InAppPurchaseActivityLifecycleTracker.startIapLogging() + } + } - @JvmStatic - fun startTracking() { - if (enabled.get()) { - if (usingBillingLib2Plus() && isEnabled(FeatureManager.Feature.IapLoggingLib2)) { - InAppPurchaseAutoLogger.startIapLogging(getApplicationContext()) - } else { - InAppPurchaseActivityLifecycleTracker.startIapLogging() - } + V5_Plus -> return + } } - } - private fun usingBillingLib2Plus(): Boolean { - return try { - val context = getApplicationContext() - val info = - context.packageManager.getApplicationInfo( - context.packageName, PackageManager.GET_META_DATA) - if (info != null) { - val version = info.metaData.getString(GOOGLE_BILLINGCLIENT_VERSION) - val versionArray = if (version === null) return false else version.split(".", limit = 3) - return versionArray[0].toInt() >= 2 - } - false - } catch (e: Exception) { - false + private fun getIAPHandler(): InAppPurchaseUtils.BillingClientVersion { + try { + val context = getApplicationContext() + val info = + context.packageManager.getApplicationInfo( + context.packageName, PackageManager.GET_META_DATA + ) + // If we can't find the package, the billing client wrapper will not be able + // to fetch any of the necessary methods/classes. + val version = info.metaData.getString(GOOGLE_BILLINGCLIENT_VERSION) + ?: return NONE + val versionArray = version.split( + ".", + limit = 3 + ) + if (version.isEmpty()) { + // Default to newest version + return V5_Plus + } + val majorVersion = + versionArray[0].toIntOrNull() ?: return V5_Plus + return if (majorVersion == 1) { + V1 + } else if (majorVersion < 5) { + V2_V4 + } else { + V5_Plus + } + } catch (e: Exception) { + // Default to newest version + return V5_Plus + } } - } } diff --git a/facebook-core/src/main/java/com/facebook/appevents/iap/InAppPurchaseUtils.kt b/facebook-core/src/main/java/com/facebook/appevents/iap/InAppPurchaseUtils.kt index ce4d964615..75e96198a1 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/iap/InAppPurchaseUtils.kt +++ b/facebook-core/src/main/java/com/facebook/appevents/iap/InAppPurchaseUtils.kt @@ -15,72 +15,79 @@ import java.lang.reflect.Method @AutoHandleExceptions object InAppPurchaseUtils { - /** Returns the Class object associated with the class or interface with the given string name */ - @JvmStatic - fun getClass(className: String): Class<*>? { - return try { - Class.forName(className) - } catch (e: ClassNotFoundException) { - null + /** Returns the Class object associated with the class or interface with the given string name */ + @JvmStatic + fun getClass(className: String): Class<*>? { + return try { + Class.forName(className) + } catch (e: ClassNotFoundException) { + null + } } - } - /** - * Returns a Method object that reflects the specified public member method of the class or - * interface represented by this Class object. - */ - @JvmStatic - fun getMethod(clazz: Class<*>, methodName: String, vararg args: Class<*>?): Method? { - return try { - clazz.getMethod(methodName, *args) - } catch (e: NoSuchMethodException) { - null + /** + * Returns a Method object that reflects the specified public member method of the class or + * interface represented by this Class object. + */ + @JvmStatic + fun getMethod(clazz: Class<*>, methodName: String, vararg args: Class<*>?): Method? { + return try { + clazz.getMethod(methodName, *args) + } catch (e: NoSuchMethodException) { + null + } } - } - /** - * Gets the declared method from class provided and returns the method to be use for invocation - */ - @JvmStatic - internal fun getDeclaredMethod( - clazz: Class<*>, - methodName: String, - vararg args: Class<*>? - ): Method? { - return try { - clazz.getDeclaredMethod(methodName, *args) - } catch (e: NoSuchMethodException) { - null + /** + * Gets the declared method from class provided and returns the method to be use for invocation + */ + @JvmStatic + internal fun getDeclaredMethod( + clazz: Class<*>, + methodName: String, + vararg args: Class<*>? + ): Method? { + return try { + clazz.getDeclaredMethod(methodName, *args) + } catch (e: NoSuchMethodException) { + null + } } - } - /** - * Invokes the underlying method represented by this Method object, on the specified object with - * the specified parameters. - */ - @JvmStatic - fun invokeMethod(clazz: Class<*>, method: Method, obj: Any?, vararg args: Any?): Any? { - var obj = obj - if (obj != null) { - obj = clazz.cast(obj) + /** + * Invokes the underlying method represented by this Method object, on the specified object with + * the specified parameters. + */ + @JvmStatic + fun invokeMethod(clazz: Class<*>, method: Method, obj: Any?, vararg args: Any?): Any? { + var obj = obj + if (obj != null) { + obj = clazz.cast(obj) + } + try { + return method.invoke(obj, *args) + } catch (e: IllegalAccessException) { + /* swallow */ + } catch (e: InvocationTargetException) { + /* swallow */ + } + return null } - try { - return method.invoke(obj, *args) - } catch (e: IllegalAccessException) { - /* swallow */ - } catch (e: InvocationTargetException) { - /* swallow */ + + /** Gets class from the context class loader and returns null if class is not found. */ + @JvmStatic + internal fun getClassFromContext(context: Context, className: String): Class<*>? { + try { + return context.classLoader.loadClass(className) + } catch (e: ClassNotFoundException) { + return null + } } - return null - } - /** Gets class from the context class loader and returns null if class is not found. */ - @JvmStatic - internal fun getClassFromContext(context: Context, className: String): Class<*>? { - try { - return context.classLoader.loadClass(className) - } catch (e: ClassNotFoundException) { - return null + enum class BillingClientVersion { + NONE, + V1, + V2_V4, + V5_Plus } - } } diff --git a/facebook-core/src/test/kotlin/com/facebook/appevents/iap/InAppPurchaseManagerTest.kt b/facebook-core/src/test/kotlin/com/facebook/appevents/iap/InAppPurchaseManagerTest.kt index 5d631b95c0..ebc00953ae 100644 --- a/facebook-core/src/test/kotlin/com/facebook/appevents/iap/InAppPurchaseManagerTest.kt +++ b/facebook-core/src/test/kotlin/com/facebook/appevents/iap/InAppPurchaseManagerTest.kt @@ -31,68 +31,91 @@ import org.powermock.reflect.Whitebox FeatureManager::class, InAppPurchaseActivityLifecycleTracker::class, InAppPurchaseAutoLogger::class, - InAppPurchaseManager::class) + InAppPurchaseManager::class +) class InAppPurchaseManagerTest : FacebookPowerMockTestCase() { - private lateinit var mockContext: Context - override fun setup() { - super.setup() - mockContext = mock() - PowerMockito.mockStatic(FeatureManager::class.java) - PowerMockito.mockStatic(InAppPurchaseActivityLifecycleTracker::class.java) - PowerMockito.mockStatic(InAppPurchaseAutoLogger::class.java) - PowerMockito.mockStatic(FeatureManager::class.java) - PowerMockito.mockStatic(FacebookSdk::class.java) - Whitebox.setInternalState(InAppPurchaseManager::class.java, "enabled", AtomicBoolean(false)) - whenever(FacebookSdk.getApplicationContext()).thenReturn(mockContext) - } + private lateinit var mockContext: Context + override fun setup() { + super.setup() + mockContext = mock() + PowerMockito.mockStatic(FeatureManager::class.java) + PowerMockito.mockStatic(InAppPurchaseActivityLifecycleTracker::class.java) + PowerMockito.mockStatic(InAppPurchaseAutoLogger::class.java) + PowerMockito.mockStatic(FeatureManager::class.java) + PowerMockito.mockStatic(FacebookSdk::class.java) + Whitebox.setInternalState(InAppPurchaseManager::class.java, "enabled", AtomicBoolean(false)) + whenever(FacebookSdk.getApplicationContext()).thenReturn(mockContext) + } + + @Test + fun `test start iap logging when billing lib 2+ is not available`() { + MemberModifier.stub( + PowerMockito.method(InAppPurchaseManager::class.java, "getIAPHandler") + ) + .toReturn(InAppPurchaseUtils.BillingClientVersion.V1) + var isStartIapLoggingCalled = false + whenever(InAppPurchaseActivityLifecycleTracker.startIapLogging()).thenAnswer { + isStartIapLoggingCalled = true + Unit + } + InAppPurchaseManager.enableAutoLogging() + assertThat(isStartIapLoggingCalled).isTrue + } - @Test - fun `test start iap logging when billing lib 2+ is not available`() { - MemberModifier.stub( - PowerMockito.method(InAppPurchaseManager::class.java, "usingBillingLib2Plus")) - .toReturn(false) - var isStartIapLoggingCalled = false - whenever(InAppPurchaseActivityLifecycleTracker.startIapLogging()).thenAnswer { - isStartIapLoggingCalled = true - Unit + @Test + fun `test start iap logging when cant find dependency`() { + MemberModifier.stub( + PowerMockito.method(InAppPurchaseManager::class.java, "getIAPHandler") + ) + .toReturn(InAppPurchaseUtils.BillingClientVersion.NONE) + var isStartIapLoggingCalled = false + whenever(InAppPurchaseActivityLifecycleTracker.startIapLogging()).thenAnswer { + isStartIapLoggingCalled = true + Unit + } + InAppPurchaseManager.enableAutoLogging() + assertThat(isStartIapLoggingCalled).isFalse } - InAppPurchaseManager.enableAutoLogging() - assertThat(isStartIapLoggingCalled).isTrue - } - @Test - fun `test start iap logging when billing lib 2+ is available but feature is off`() { - MemberModifier.stub( - PowerMockito.method(InAppPurchaseManager::class.java, "usingBillingLib2Plus")) - .toReturn(true) - whenever(FeatureManager.isEnabled(FeatureManager.Feature.IapLoggingLib2)).thenReturn(false) - var isStartIapLoggingCalled = false - whenever(InAppPurchaseActivityLifecycleTracker.startIapLogging()).thenAnswer { - isStartIapLoggingCalled = true - Unit + @Test + fun `test start iap logging when billing lib 2+ is available but feature is off`() { + MemberModifier.stub( + PowerMockito.method(InAppPurchaseManager::class.java, "getIAPHandler") + ) + .toReturn(InAppPurchaseUtils.BillingClientVersion.V2_V4) + whenever(FeatureManager.isEnabled(FeatureManager.Feature.IapLoggingLib2)).thenReturn(false) + var isStartIapLoggingCalled = false + whenever(InAppPurchaseActivityLifecycleTracker.startIapLogging()).thenAnswer { + isStartIapLoggingCalled = true + Unit + } + InAppPurchaseManager.enableAutoLogging() + assertThat(isStartIapLoggingCalled).isTrue } - InAppPurchaseManager.enableAutoLogging() - assertThat(isStartIapLoggingCalled).isTrue - } - @Test - fun `test start iap logging when billing lib 2+ is available and feature is on`() { - whenever(FeatureManager.isEnabled(FeatureManager.Feature.IapLoggingLib2)).thenReturn(true) - val mockPackageManager: PackageManager = mock() - val mockApplicationInfo = ApplicationInfo() - val metaData = Bundle() - metaData.putString("com.google.android.play.billingclient.version", "2.0.3") - whenever(mockContext.packageManager).thenReturn(mockPackageManager) - whenever(mockContext.packageName).thenReturn("com.facebook.test") - whenever(mockPackageManager.getApplicationInfo(any(), any())).thenReturn(mockApplicationInfo) - mockApplicationInfo.metaData = metaData + @Test + fun `test start iap logging when billing lib 2+ is available and feature is on`() { + whenever(FeatureManager.isEnabled(FeatureManager.Feature.IapLoggingLib2)).thenReturn(true) + val mockPackageManager: PackageManager = mock() + val mockApplicationInfo = ApplicationInfo() + val metaData = Bundle() + metaData.putString("com.google.android.play.billingclient.version", "2.0.3") + whenever(mockContext.packageManager).thenReturn(mockPackageManager) + whenever(mockContext.packageName).thenReturn("com.facebook.test") + whenever( + mockPackageManager.getApplicationInfo( + any(), + any() + ) + ).thenReturn(mockApplicationInfo) + mockApplicationInfo.metaData = metaData - var isStartIapLoggingCalled = false - whenever(InAppPurchaseAutoLogger.startIapLogging(any())).thenAnswer { - isStartIapLoggingCalled = true - Unit + var isStartIapLoggingCalled = false + whenever(InAppPurchaseAutoLogger.startIapLogging(any())).thenAnswer { + isStartIapLoggingCalled = true + Unit + } + InAppPurchaseManager.enableAutoLogging() + assertThat(isStartIapLoggingCalled).isTrue } - InAppPurchaseManager.enableAutoLogging() - assertThat(isStartIapLoggingCalled).isTrue - } }