Skip to content

Commit

Permalink
Create enum to handle different versions of GPBL
Browse files Browse the repository at this point in the history
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
  • Loading branch information
maxalbrightmeta authored and facebook-github-bot committed Aug 28, 2024
1 parent 838c52c commit fa1ebff
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<InAppPurchaseUtils.BillingClientVersion>(
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<Boolean>(
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<InAppPurchaseUtils.BillingClientVersion>(
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<Boolean>(
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<InAppPurchaseUtils.BillingClientVersion>(
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
}
}

0 comments on commit fa1ebff

Please sign in to comment.