当 Play 计费库调用触发操作时,库会返回 BillingResult
响应以告知开发者结果。例如,如果您使用 queryProductDetailsAsync
获取用户的可用优惠,响应码将包含 OK 代码并提供正确的 ProductDetails
对象,或者包含不同的响应,指示无法提供 ProductDetails
对象的原因。
并非所有响应码都是错误。 BillingResponseCode
参考页面提供了本指南中讨论的每个响应的详细说明。一些不表示错误的响应码示例是
BillingClient.BillingResponseCode.OK
:调用触发的操作已成功完成。BillingClient.BillingResponseCode.USER_CANCELED
:对于向用户显示 Play 商店 UI 流程的操作,此响应表示用户在完成流程之前离开了这些 UI 流程。
当响应码确实表示错误时,原因有时是由于瞬态条件造成的,因此可以恢复。当对 Play 计费库方法的调用返回 BillingResponseCode
值,该值表示可恢复的条件时,您应该重试调用。在其他情况下,不认为条件是瞬态的,因此不建议重试。
瞬态错误需要不同的重试策略,具体取决于因素,例如错误是否发生在用户处于会话状态下(例如,用户正在进行购买流程)或错误是否发生在后台(例如,您在onResume
期间查询用户的现有购买)。下面的 重试策略部分 提供了这些不同策略的示例,以及 可重试的 BillingResult 响应部分 建议了哪种策略最适合每个响应码。
除了响应码之外,一些错误响应还包括用于调试和记录目的的消息。
重试策略
简单重试
在用户处于会话状态的情况下,最好实施简单的重试策略,这样错误对用户体验的影响尽可能小。在这种情况下,我们建议使用简单的重试策略,并使用最大尝试次数作为退出条件。
以下示例演示了在建立 BillingClient
连接时处理错误的简单重试策略
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
// Initialize the BillingClient.
private val billingClient = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
// Establish a connection to Google Play.
fun startBillingConnection() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d(TAG, "Billing response OK")
// The BillingClient is ready. You can now query Products Purchases.
} else {
Log.e(TAG, billingResult.debugMessage)
retryBillingServiceConnection()
}
}
override fun onBillingServiceDisconnected() {
Log.e(TAG, "GBPL Service disconnected")
retryBillingServiceConnection()
}
})
}
// Billing connection retry logic. This is a simple max retry pattern
private fun retryBillingServiceConnection() {
val maxTries = 3
var tries = 1
var isConnectionEstablished = false
do {
try {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
isConnectionEstablished = true
Log.d(TAG, "Billing connection retry succeeded.")
} else {
Log.e(
TAG,
"Billing connection retry failed: ${billingResult.debugMessage}"
)
}
}
})
} catch (e: Exception) {
e.message?.let { Log.e(TAG, it) }
tries++
}
} while (tries <= maxTries && !isConnectionEstablished)
}
...
}
指数退避重试
我们建议对在后台运行且不会影响用户体验的 Play 结算库操作使用指数退避。
例如,在确认新购买时实施此操作是合适的,因为此操作可以在后台进行,并且如果发生错误,确认不需要实时进行。
private fun acknowledge(purchaseToken: String): BillingResult {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build()
var ackResult = BillingResult()
billingClient.acknowledgePurchase(params) { billingResult ->
ackResult = billingResult
}
return ackResult
}
suspend fun acknowledgePurchase(purchaseToken: String) {
val retryDelayMs = 2000L
val retryFactor = 2
val maxTries = 3
withContext(Dispatchers.IO) {
acknowledge(purchaseToken)
}
AcknowledgePurchaseResponseListener { acknowledgePurchaseResult ->
val playBillingResponseCode =
PlayBillingResponseCode(acknowledgePurchaseResult.responseCode)
when (playBillingResponseCode) {
BillingClient.BillingResponseCode.OK -> {
Log.i(TAG, "Acknowledgement was successful")
}
BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
// This is possibly related to a stale Play cache.
// Querying purchases again.
Log.d(TAG, "Acknowledgement failed with ITEM_NOT_OWNED")
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
{ billingResult, purchaseList ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
purchaseList.forEach { purchase ->
acknowledge(purchase.purchaseToken)
}
}
}
}
}
in setOf(
BillingClient.BillingResponseCode.ERROR,
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
) -> {
Log.d(
TAG,
"Acknowledgement failed, but can be retried --
Response Code: ${acknowledgePurchaseResult.responseCode} --
Debug Message: ${acknowledgePurchaseResult.debugMessage}"
)
runBlocking {
exponentialRetry(
maxTries = maxTries,
initialDelay = retryDelayMs,
retryFactor = retryFactor
) { acknowledge(purchaseToken) }
}
}
in setOf(
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE,
BillingClient.BillingResponseCode.DEVELOPER_ERROR,
BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED,
) -> {
Log.e(
TAG,
"Acknowledgement failed and cannot be retried --
Response Code: ${acknowledgePurchaseResult.responseCode} --
Debug Message: ${acknowledgePurchaseResult.debugMessage}"
)
throw Exception("Failed to acknowledge the purchase!")
}
}
}
}
private suspend fun <T> exponentialRetry(
maxTries: Int = Int.MAX_VALUE,
initialDelay: Long = Long.MAX_VALUE,
retryFactor: Int = Int.MAX_VALUE,
block: suspend () -> T
): T? {
var currentDelay = initialDelay
var retryAttempt = 1
do {
runCatching {
delay(currentDelay)
block()
}
.onSuccess {
Log.d(TAG, "Retry succeeded")
return@onSuccess;
}
.onFailure { throwable ->
Log.e(
TAG,
"Retry Failed -- Cause: ${throwable.cause} -- Message: ${throwable.message}"
)
}
currentDelay *= retryFactor
retryAttempt++
} while (retryAttempt < maxTries)
return block() // last attempt
}
可重试的 BillingResult 响应
NETWORK_ERROR (错误代码 12)
问题
此错误表明设备与 Play 系统之间存在网络连接问题。
可能的解决方案
为了恢复,请使用简单的重试或指数退避,具体取决于触发错误的操作。
SERVICE_TIMEOUT (错误代码 -3)
问题
此错误表明请求已达到最大超时时间,但 Google Play 尚未响应。例如,这可能是由 Play 结算库调用请求的操作执行延迟造成的。
可能的解决方案
这通常是一个短暂的问题。请使用简单或指数退避策略重新尝试请求,具体取决于返回错误的操作。
与下面的 SERVICE_DISCONNECTED
不同,与 Google Play 结算服务的连接不会断开,您只需要重新尝试任何尝试过的 Play 结算库操作即可。
SERVICE_DISCONNECTED (错误代码 -1)
问题
此致命错误表明客户端应用通过 BillingClient
与 Google Play 商店服务的连接已断开。
可能的解决方案
为了尽可能避免此错误,请始终在使用 Play 结算库进行调用之前,通过调用 BillingClient.isReady()
检查与 Google Play 服务的连接。
为了尝试从 SERVICE_DISCONNECTED
恢复,您的客户端应用应尝试使用 BillingClient.startConnection
重新建立连接。
与 SERVICE_TIMEOUT
一样,请使用简单的重试或指数退避,具体取决于触发错误的操作。
SERVICE_UNAVAILABLE (错误代码 2)
重要提示
从 Google Play 结算库 6.0.0 开始,SERVICE_UNAVAILABLE
不再用于返回网络问题。它是在结算服务不可用且已弃用的 SERVICE_TIMEOUT
案例场景中返回的。
问题
此瞬态错误表明 Google Play 结算服务当前不可用。在大多数情况下,这意味着客户端设备与 Google Play 结算服务之间的任何地方都存在网络连接问题。
可能的解决方案
这通常是一个短暂的问题。请使用简单或指数退避策略重新尝试请求,具体取决于返回错误的操作。
与 SERVICE_DISCONNECTED
不同,与 Google Play 结算服务的连接不会断开,您需要重新尝试任何正在尝试的操作。
BILLING_UNAVAILABLE (错误代码 3)
问题
此错误表明在购买过程中发生了用户结算错误。以下是一些可能发生此错误的情况:
- 用户设备上的 Play 商店应用已过时。
- 用户位于不受支持的国家/地区。
- 用户是企业用户,他们的企业管理员已禁用用户进行购买。
- Google Play 无法对用户的付款方式进行收费。例如,用户的信用卡可能已过期。
可能的解决方案
在这种情况下,自动重试不太可能有所帮助。但是,如果用户解决了导致问题的情况,手动重试可能会有所帮助。例如,如果用户将 Play 商店版本更新为支持的版本,那么手动重试初始操作可能会起作用。
如果在用户未处于会话状态时发生此错误,重试可能没有意义。当您收到 BILLING_UNAVAILABLE
错误作为购买流程的结果时,用户很可能在购买过程中收到了来自 Google Play 的反馈,并且可能已经知道出了什么问题。在这种情况下,您可以显示一条错误消息,说明出了问题,并提供一个“重试”按钮,让用户在解决问题后选择手动重试。
ERROR (错误代码 6)
问题
这是一个致命错误,表明 Google Play 本身存在内部问题。
可能的解决方案
有时,导致 ERROR
的 Google Play 内部问题是短暂的,可以使用指数退避进行重试以进行缓解。当用户处于会话状态时,简单的重试是更好的选择。
ITEM_ALREADY_OWNED
问题
此响应表明 Google Play 用户已经拥有他们尝试购买的订阅或一次性购买产品。在大多数情况下,这不是一个瞬态错误,除非它是由于 Google Play 的缓存过时造成的。
可能的解决方案
为了避免在原因不是缓存问题时发生此错误,请不要在用户已经拥有该产品时提供该产品以供购买。确保您在显示可供购买的产品时检查用户的权利,并相应地过滤用户可以购买的内容。当客户端应用由于缓存问题而收到此错误时,该错误会触发 Google Play 的缓存以使用来自 Play 后端的最新数据进行更新。在这种情况下,错误后重试应该解决此特定瞬态实例。调用 BillingClient.queryPurchasesAsync()
后获得 ITEM_ALREADY_OWNED
以检查用户是否已获得产品,如果他们没有获得产品,请实施简单的重试逻辑以重新尝试购买。
ITEM_NOT_OWNED
问题
此购买响应表明 Google Play 用户没有拥有用户尝试替换、确认或消费的订阅或一次性购买产品。在大多数情况下,这不是一个瞬态错误,除非它是由于 Google Play 的缓存进入过时状态造成的。
可能的解决方案
当由于缓存问题而收到错误时,该错误会触发 Google Play 的缓存以使用来自 Play 后端的最新数据进行更新。错误后使用简单的重试策略进行重试应该解决此特定瞬态实例。调用 BillingClient.queryPurchasesAsync()
后获得 ITEM_NOT_OWNED
以检查用户是否已获得产品。如果他们没有获得产品,请使用简单的重试逻辑以重新尝试购买。
不可重试的 BillingResult 响应
您无法使用重试逻辑从这些错误中恢复。
FEATURE_NOT_SUPPORTED
问题
此不可重试错误表明 Google Play 结算功能在用户设备上不受支持,可能是由于 Play 商店版本过旧造成的。
例如,也许您的一些用户设备不支持应用内消息传递。
可能的缓解措施
在对 Play 结算库进行调用之前,使用 BillingClient.isFeatureSupported()
检查功能支持。
when {
billingClient.isReady -> {
if (billingClient.isFeatureSupported(BillingClient.FeatureType.IN_APP_MESSAGING)) {
// use feature
}
}
}
USER_CANCELED
问题
用户已退出结算流程 UI。
可能的解决方案
这仅供参考,可以顺利失败。
ITEM_UNAVAILABLE
问题
Google Play 结算订阅或一次性购买产品对于此用户不可用。
可能的缓解措施
确保您的应用通过 queryProductDetailsAsync
刷新产品详细信息,如建议的那样。考虑 Play Console 配置中的产品目录更改频率以实施额外的刷新(如果需要)。仅尝试在 Google Play 结算上出售通过 queryProductDetailsAsync
返回正确信息的 Google Play 结算产品。检查产品资格配置是否存在任何不一致。例如,您可能正在查询仅适用于用户尝试购买的区域以外的区域的产品。要可供购买,产品需要处于活动状态,其应用需要发布,并且其应用需要在用户的国家/地区可用。
有时,特别是在测试过程中,产品配置中的所有内容都正确,但用户仍然会看到此错误。这可能是由于产品详细信息在 Google 的服务器之间传播延迟造成的。请稍后再试。
DEVELOPER_ERROR
问题
这是一个致命错误,表明您正在不正确地使用 API。例如,对 BillingClient.launchBillingFlow
提供不正确的参数会导致此错误。
可能的解决方案
确保您正确使用不同的 Play 结算库调用。另外,检查调试消息以获取有关错误的更多信息。