Google IAP Billing Library 5:待处理转可计费购买消费时触发SERVICE_DISCONNECTED
Google IAP Billing Library 5:PENDING转CHARGEABLE购买消费时SERVICE_DISCONNECTED错误修复
问题详情
使用Google IAP Billing Library 5,代码基本照搬官方文档,但处理从PENDING状态转为CHARGEABLE状态的内购时,调用consumeAsync总会返回SERVICE_DISCONNECTED错误。实际服务并未真正断开(onBillingServiceDisconnected很久才触发),强制重连后反而查不到CHARGEABLE状态的购买,但完全退出再重启APP,就能正常检测到并成功消费。
注:未经历PENDING状态的测试支付,消费流程完全正常。
原代码:
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) { runOnUiThread(new Runnable() { @Override public void run() { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {toastUI("Purchase Canceled"); return;} if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK || purchases == null) {toastUI("Error: " + billingResult.getDebugMessage()); return; } // only option left is BillingClient.BillingResponseCode.OK for (Purchase purchase : purchases) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) header.setText("Pending Purchase; you may return later to complete "); else { ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build(); ConsumeResponseListener listener = (billingResult1, purchaseToken) -> { if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.OK) { header.setText("Purchase Complete: " + purchase.getProducts()); Log.e(TAG, "purchase complete"); //TODO now we just need to apply the purchase to the main game } else if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) { Log.e(TAG, "Server disconnected, retrying"); header.setText("Server disconnected, retrying "+ billingResult1.toString()); billingClient.startConnection(IAP_Activity.this); } else header.setText("purchase consumption failed: \n " + billingResult1.getDebugMessage()); }; billingClient.consumeAsync(consumeParams, listener); } }}}); // run on UI thread }
问题根源
这个SERVICE_DISCONNECTED是框架的误报,核心原因是:
- 当购买从PENDING转为CHARGEABLE时,
onPurchasesUpdated回调返回的Purchase对象可能是PENDING状态时的旧实例,用这个旧实例的token直接消费,会触发服务端校验失败,被包装成SERVICE_DISCONNECTED错误。 - 强制重连后直接消费,本地BillingClient缓存还没更新最新的购买状态,所以查不到CHARGEABLE的订单;重启APP后,BillingClient会重新拉取服务器端的最新购买数据,因此能正常处理。
修复方案
1. 持久化保存PENDING状态的购买Token
当收到PENDING状态的购买时,把该订单的purchaseToken保存到本地(比如SharedPreferences或数据库),后续用于查询最新状态:
// 示例:用SharedPreferences保存 SharedPreferences prefs = getSharedPreferences("IAP_PREFS", MODE_PRIVATE); if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) { header.setText("Pending Purchase; you may return later to complete "); prefs.edit().putString("PENDING_PURCHASE_TOKEN", purchase.getPurchaseToken()).apply(); }
2. 主动查询最新购买状态再消费
不要直接用onPurchasesUpdated返回的Purchase对象消费,而是调用queryPurchasesAsync拉取服务器端最新的购买数据,拿到CHARGEABLE状态的实例后再执行消费:
// 定义查询未消费购买的方法 private void queryAndConsumePendingPurchases() { if (!billingClient.isReady()) { Log.e(TAG, "Billing client not ready, skip query"); return; } billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.INAPP) .build(), (billingResult, purchases) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) { for (Purchase purchase : purchases) { if (purchase.getPurchaseState() == Purchase.PurchaseState.CHARGEABLE && !purchase.isAcknowledged()) { // 执行消费 consumePurchase(purchase); // 消费成功后清除本地保存的token getSharedPreferences("IAP_PREFS", MODE_PRIVATE) .edit().remove("PENDING_PURCHASE_TOKEN").apply(); } } } }); } // 单独封装消费逻辑 private void consumePurchase(Purchase purchase) { ConsumeParams consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(); billingClient.consumeAsync(consumeParams, (billingResult1, purchaseToken) -> { runOnUiThread(() -> { if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.OK) { header.setText("Purchase Complete: " + purchase.getProducts()); Log.e(TAG, "purchase complete"); // 应用购买权益到游戏 } else { header.setText("purchase consumption failed: \n " + billingResult1.getDebugMessage()); } }); }); }
3. 重连后触发购买查询
当收到SERVICE_DISCONNECTED错误时,重连成功后要主动查询购买状态,而不是直接重试消费:
// 修改原代码中的SERVICE_DISCONNECTED处理逻辑 else if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) { Log.e(TAG, "Server disconnected, reconnecting..."); header.setText("Server disconnected, reconnecting..."); billingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResult) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { // 连接成功后查询最新购买状态 queryAndConsumePendingPurchases(); } } @Override public void onBillingServiceDisconnected() { // 可以加入重连重试逻辑 } }); }
4. 在APP启动/连接成功时自动查询
在onBillingSetupFinished回调里,自动触发一次购买查询,确保APP启动后能检测到所有待处理的CHARGEABLE订单:
@Override public void onBillingSetupFinished(BillingResult billingResult) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { // 连接成功后查询待处理的购买 queryAndConsumePendingPurchases(); } }
修改后的完整onPurchasesUpdated代码
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) { runOnUiThread(() -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) { toastUI("Purchase Canceled"); return; } if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK || purchases == null) { toastUI("Error: " + billingResult.getDebugMessage()); return; } SharedPreferences prefs = getSharedPreferences("IAP_PREFS", MODE_PRIVATE); for (Purchase purchase : purchases) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) { header.setText("Pending Purchase; you may return later to complete "); // 保存PENDING订单的token prefs.edit().putString("PENDING_PURCHASE_TOKEN", purchase.getPurchaseToken()).apply(); } else { // 不直接消费,而是触发查询最新状态后消费 queryAndConsumePendingPurchases(); } } }); }
内容的提问来源于stack exchange,提问作者Al Ro
相关产品推荐
相关产品推荐

