BillingClient.BillingClientStateListener.onBillingSetupFinished被多次调用。

14

我尝试使用Kotlin协程为BillingClient v.2.2.0编写包装器:

package com.cantalk.photopose.billing

import android.app.Activity
import android.content.Context
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient.*
import com.cantalk.photopose.util.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

class BillingClientAsync(context: Context) {
    private val billingClient: BillingClient = setupBillingClient(context)
    private val pendingPurchaseFlows = HashMap<String, CompletableDeferred<Purchase>>()

    private fun setupBillingClient(context: Context): BillingClient {
        return newBuilder(context)
            .enablePendingPurchases()
            .setListener { billingResult, purchases ->
                if (billingResult.responseCode == BillingResponseCode.OK && purchases != null) {
                    for (purchase in purchases) {
                        val deferred = pendingPurchaseFlows.remove(purchase.sku)
                        deferred?.complete(purchase)
                    }
                } else {
                    val iterator = pendingPurchaseFlows.iterator()
                    while (iterator.hasNext()) {
                        val entry = iterator.next()
                        entry.value.completeExceptionally(BillingException(billingResult))
                        iterator.remove()
                    }
                }
            }
            .build()
    }

    suspend fun queryPurchases(): List<Purchase> {
        Logger.debug("query purchases")

        ensureConnected()
        val queryPurchases = billingClient.queryPurchases(SkuType.INAPP)
        if (queryPurchases.responseCode == BillingResponseCode.OK) {
            return queryPurchases.purchasesList
        } else {
            throw BillingException(queryPurchases.billingResult)
        }
    }

    suspend fun querySkuDetails(@SkuType type: String, skus: List<String>): List<SkuDetails> {
        Logger.debug("query sku details for", type)

        ensureConnected()
        return suspendCoroutine { continuation ->
            val params = SkuDetailsParams.newBuilder()
                .setType(type)
                .setSkusList(skus)
                .build()
            billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
                if (billingResult.responseCode == BillingResponseCode.OK) {
                    continuation.resume(skuDetailsList)
                } else {
                    continuation.resumeWithException(BillingException(billingResult))
                }
            }
        }
    }

    suspend fun purchase(activity: Activity, skuDetails: SkuDetails): Purchase {
        Logger.debug("purchase", skuDetails.sku)

        ensureConnected()
        val currentPurchaseFlow = CompletableDeferred<Purchase>()
            .also { pendingPurchaseFlows[skuDetails.sku] = it }
        val params = BillingFlowParams.newBuilder()
            .setSkuDetails(skuDetails)
            .build()
        billingClient.launchBillingFlow(activity, params)
        return currentPurchaseFlow.await()
    }

    suspend fun consume(purchase: Purchase): String {
        Logger.debug("consume", purchase.sku)

        ensureConnected()
        return suspendCoroutine { continuation ->
            val params = ConsumeParams.newBuilder()
                .setPurchaseToken(purchase.purchaseToken)
                .setDeveloperPayload("TBD")
                .build()
            billingClient.consumeAsync(params) { billingResult, purchaseToken ->
                if (billingResult.responseCode == BillingResponseCode.OK) {
                    continuation.resume(purchaseToken)
                } else {
                    continuation.resumeWithException(BillingException(billingResult))
                }
            }
        }
    }

    suspend fun acknowledgePurchase(purchase: Purchase) {
        Logger.debug("acknowledge", purchase.sku)

        ensureConnected()
        return suspendCoroutine { continuation ->
            val params = AcknowledgePurchaseParams.newBuilder()
                .setPurchaseToken(purchase.purchaseToken)
                .setDeveloperPayload("TBD")
                .build()
            billingClient.acknowledgePurchase(params) { billingResult ->
                if (billingResult.responseCode == BillingResponseCode.OK) {
                    continuation.resume(Unit)
                } else {
                    continuation.resumeWithException(BillingException(billingResult))
                }
            }
        }
    }

    private suspend fun ensureConnected() {
        if (!billingClient.isReady) {
            startConnection()
        }
    }

    private suspend fun startConnection() {
        Logger.debug("connect to billing service")

        return suspendCoroutine { continuation ->
            billingClient.startConnection(object : BillingClientStateListener {
                override fun onBillingSetupFinished(billingResult: BillingResult) {
                    if (billingResult.responseCode == BillingResponseCode.OK) {
                        continuation.resume(Unit)
                    } else {
                        // TODO: 3 Google Play In-app Billing API version is less than 3
                        continuation.resumeWithException(BillingException(billingResult))
                    }
                }

                override fun onBillingServiceDisconnected() = Unit
            })
        }
    }
}

如您所见,当我尝试查询购买或购买时,我会确保客户端已准备好。但是在生产中会出现许多错误:

java.lang.IllegalStateException: 
  at kotlin.coroutines.SafeContinuation.resumeWith (SafeContinuation.java:2)
  at com.cantalk.photopose.billing.BillingClientAsync$startConnection$2$1.onBillingSetupFinished (BillingClientAsync.java:2)
  at com.android.billingclient.api.zzai.run (zzai.java:6)

我试图理解问题的原因,并发现如果调用BillingClientStateListener.onBillingSetupFinished多次,可能会出现异常IllegalStateException: Already resumed。我想知道这是如何可能的,因为我每次使用startConnection都创建了新的监听器?我无法在模拟器或测试设备上重现此问题。有人能向我解释发生了什么以及如何修复吗?


你是否在最新版本的计费中遇到了相同的问题,而不是旧的2.2.0版本?https://developer.android.com/google/play/billing/release-notes - PerracoLabs
一样的情况,是的,所有东西都是最新版本。同样的问题:无法复现,但用户看到了它。 - Gábor
3个回答

6
起初,我也尝试过同样的做法,但这种做法的理由是不正确的。设计上, onBillingSetupFinished() 可能会被多次调用。一旦你使用回调调用BillingClient.startConnection(BillingClientStateListener) 并将其存储在内部,如果连接断开/恢复时会再次调用该回调。你不应该在其他调用BillingClient.startConnection(BillingClientStateListener) 时传入一个新对象。
请阅读关于onBillingServiceDisconnected() 的文档:
称为通知已丢失与结算服务的连接。
注意:这并不会删除结算服务连接本身-这个绑定到服务的连接将保持活动状态,并且在下次运行结算服务并完成设置时,你将收到对 onBillingSetupFinished(BillingResult) 的调用。
这意味着当连接断开然后稍后重新获得连接时,onBillingSetupFinished(BillingResult) 将再次被调用,在你的实现中,你将再次尝试恢复协程,但是协程已经被恢复,你将得到一个 IllegalStateException
我最终所做的是在类本身中实现 BillingClientStateListener 接口,并在回调中使用 BillingResult 更新 SharedFlow<Int>
private val billingClientStatus = MutableSharedFlow<Int>(
    replay = 1,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

override fun onBillingSetupFinished(result: BillingResult) {
    billingClientStatus.tryEmit(result.responseCode)
}

override fun onBillingServiceDisconnected() {
    billingClientStatus.tryEmit(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
}

接下来,您可以收集流程来获取SKU价格,或处理挂起的购买订单(如果与计费客户端连接),或者在未连接时实施重试逻辑:

init {
    billingClientStatus.tryEmit(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
    lifecycleOwner.lifecycleScope.launchWhenStarted {
        billingClientStatus.collect {
            when (it) {
                BillingClient.BillingResponseCode.OK -> with (billingClient) {
                    updateSkuPrices()
                    handlePurchases()
                }
                else -> billingClient.startConnection(this)
            }
        }
    }
}

如果您正在执行需要计费客户端连接的操作,可以通过以下方式等待它:

private suspend fun requireBillingClientSetup(): Boolean =
    withTimeoutOrNull(TIMEOUT_MILLIS) {
        billingClientStatus.first { it == BillingClient.BillingResponseCode.OK }
        true
    } ?: false

(请注意,我使用了SharedFlow<T>而不是StateFlow<T>来处理billingClientStatus:原因是StateFlow<T>不支持连续发出相等的值。)

2

我的设置略有不同(我使用布尔值进行回调),但这是我能想到的最好方法。更像是一个解决方法而不是真正的解释:

private suspend fun start(): Boolean = suspendCoroutine {
  billingClient.startConnection(object : BillingClientStateListener {
    var resumed = false;

    override fun onBillingSetupFinished(billingResult: BillingResult) {
      if (!resumed) {
        it.resume(billingResult.responseCode == BillingResponseCode.OK)
        resumed = true
      }
    }

    override fun onBillingServiceDisconnected() {
      Toast.makeText(context, R.string.pay_error, Toast.LENGTH_SHORT).show()
      Log.e(LOG_TAG, "Billing disconnected")
    }
  })
}

仍有争议是否它是一个真正的、长期的解决方案。

尽管看起来很优雅,但这可能会导致IllegalStateException:onBillingSetupFinished()不能保证仅调用一次,因此您可能会尝试恢复已经恢复的续订。void onBillingServiceDisconnected() 通知连接到计费服务的连接已丢失。注意:这不会删除计费服务连接本身 - 此绑定到服务的连接将保持活动状态,并且当计费服务再次运行并完成设置时,您将收到对onBillingSetupFinished(BillingResult)的调用。 - Patrick Steiger

0

最后我用了try catch捕获IllegalStateException异常并记录日志...

val result = suspendCoroutine<Boolean> { continuation ->
    billingClient.startConnection(object : BillingClientStateListener {
        override fun onBillingServiceDisconnected() {
            WttjLogger.v("onBillingServiceDisconnected")
            // Do not call continuation resume, as per documentation
            // https://developer.android.com/reference/com/android/billingclient/api/BillingClientStateListener#onbillingservicedisconnected
        }

        override fun onBillingSetupFinished(billingResult: BillingResult) {
            val responseCode = billingResult.responseCode
            val debugMessage = billingResult.debugMessage
            if (responseCode != BillingClient.BillingResponseCode.OK) {
                WttjLogger.w("onBillingSetupFinished: $responseCode $debugMessage")
            } else {
                WttjLogger.v("onBillingSetupFinished: $responseCode $debugMessage")
            }
            try {
                continuation.resume(responseCode == BillingClient.BillingResponseCode.OK)
            } catch (e: IllegalStateException) {
                WttjLogger.e(e)
            }
        }
    })
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接