BillingClient.BillingClientStateListener.onBillingSetupFinished 被多次调用
Posted
技术标签:
【中文标题】BillingClient.BillingClientStateListener.onBillingSetupFinished 被多次调用【英文标题】:BillingClient.BillingClientStateListener.onBillingSetupFinished is called multiple times 【发布时间】:2020-08-06 20:41:42 【问题描述】:我尝试使用 Kotlin Coroutines 为 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 版本有同样的问题? developer.android.com/google/play/billing/release-notes 这里一样,是的,所有内容的最新版本。同样的问题:无法重现,但用户可以看到。 【参考方案1】:我一开始也尝试这样做,但理由不正确。根据设计,onBillingSetupFinished()
可能会被多次调用。一旦您使用回调调用BillingClient.startConnection(BillingClientStateListener)
,它会在内部存储回调,并在连接断开/重新获得时再次调用它。您不应该在对 BillingClient.startConnection(BillingClientStateListener)
的其他调用中传入新对象。
阅读onBillingServiceDisconnected()
上的文档:
调用以通知与计费服务的连接已丢失。
注意:这不会删除计费服务连接本身 - 此与服务的绑定将保持活动状态,您将收到一个 计费服务时调用 onBillingSetupFinished(BillingResult) 正在下一次运行,设置完成。
这意味着当连接断开然后重新恢复时,onBillingSetupFinished(BillingResult)
将被再次调用,并且,在您的实现中,您将尝试再次恢复协程,但协程续接已经恢复,您将获取IllegalStateException
。
我最终做的是在类本身中实现BillingClientStateListener
接口,并在回调中使用BillingResult
从onBillingSetupFinished(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
(请注意,billingClientStatus
使用 SharedFlow<T>
而不是 StateFlow<T>
:原因是 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) 的调用。【参考方案3】:我最终尝试捕获 IllegalStateException + log...
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)
)
【讨论】:
以上是关于BillingClient.BillingClientStateListener.onBillingSetupFinished 被多次调用的主要内容,如果未能解决你的问题,请参考以下文章