当 Play 结算库调用触发操作时,该库会返回 BillingResult
响应,并将结果告知开发者。例如,如果您使用 queryProductDetailsAsync
获取用户可用的优惠,则响应代码包含 OK 代码,并提供正确的 ProductDetails
对象;或者包含其他响应,指示无法提供 ProductDetails
对象的原因。
并非所有响应代码都是错误。BillingResponseCode
参考页面详细说明了本指南中讨论的每个响应。下面列举了一些未指示错误的响应代码:
BillingClient.BillingResponseCode.OK
:调用触发的操作已成功完成。BillingClient.BillingResponseCode.USER_CANCELED
:对于向用户显示 Play 商店界面流程的操作,此响应表示用户没有完成流程就离开了这些界面流程。
当响应代码指示出现错误时,有时是因为暂时性条件而导致的,因此可以恢复。调用 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 商店服务的连接已中断。
可能的解决方案
为尽可能避免此错误,请务必通过调用 BillingClient.isReady()
先检查与 Google Play 服务的连接,然后再使用 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 后端的最新数据进行更新。在这种情况下,在出现错误后重试应该能解决这个特定的瞬态情况。获得 ITEM_ALREADY_OWNED
后调用 BillingClient.queryPurchasesAsync()
,检查用户是否已经购买了相应商品,如果没有,请实现一个简单的重试逻辑来重新尝试购买。
ITEM_NOT_OWNED
问题
此购买响应表明,Google Play 用户并不拥有用户尝试替换、确认或使用的订阅或一次性购买商品。在大多数情况下,这并非暂时性错误,除非它是由 Google Play 的缓存进入过时状态导致的。
可能的解决方案
如果由于缓存问题而收到此错误,此错误会触发 Google Play 的缓存,以便使用来自 Play 后端的最新数据进行更新。出错后,通过一个简单的重试策略重试应能解决此特定瞬态错误。获得 ITEM_NOT_OWNED
后调用 BillingClient.queryPurchasesAsync()
,检查用户是否已获取相应商品。如果没有,使用简单的重试逻辑重新尝试购买。
不可重试的 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
问题
用户已退出结算流程界面。
可能的解决方案
此解决方案仅供参考,可能会以一种不引起中断的方式失败。
ITEM_UNAVAILABLE
问题
此用户无法购买 Google Play 结算服务订阅或一次性购买商品。
可能的缓解措施
请确保您的应用按照建议通过 queryProductDetailsAsync
刷新商品详情。请考虑您的商品详情在 Play 管理中心配置上发生更改的频率,以在需要时实现额外的刷新。
仅尝试在 Google Play 结算服务上销售通过 queryProductDetailsAsync
返回正确信息的商品。检查商品资格配置是否存在不一致问题。例如,您可能正在查询某个商品,该商品仅在用户尝试购买的地区之外销售。若要让商品可供购买,必须将商品的状态设为有效,同时其所属的应用必须已发布并已在用户所在的国家/地区上架。
有时,尤其是在测试期间,商品配置一切都正确无误,但用户仍会看到此错误。这可能是由于商品详情在 Google 服务器上的传播延迟所致。请稍后再试。
DEVELOPER_ERROR
问题
这是一个严重错误,表明您未正确使用 API。例如,向 BillingClient.launchBillingFlow
提供不正确的参数可能会导致此错误。
可能的解决方案
确保您正确地使用了不同的 Play 结算库调用。此外,如需详细了解此错误,请查看调试消息。