Google Play Billing PurchasesUpdatedListener返回SERVICE_DISCONNECTED问题
Google Play Billing集成问题:SERVICE_DISCONNECTED与Purchase Canceled提示
近几日在为应用集成Google Play Billing,此前曾遇到支付流程打开真实支付方式而非测试卡的问题。修改handlePurchase()方法、PurchasesUpdatedListener并添加Verify类实现购买验证后,PurchasesUpdatedListener返回了SERVICE_DISCONNECTED提示,且整个流程中仅弹出Purchase Canceled的Toast。
相关代码
HelpActivity.Java
public class HelpActivity extends AppCompatActivity { AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() { @Override public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) { Toast.makeText(HelpActivity.this, "Acknowledge", Toast.LENGTH_SHORT).show(); } }; private BillingClient billingClient; private final PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() { @Override public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) { for (Purchase purchase : purchases) { handlePurchase(purchase); } } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) { // Handle an error caused by a user cancelling the purchase flow. Toast.makeText(HelpActivity.this, "Purchase Canceled", Toast.LENGTH_SHORT).show(); //Note!!!! only this toast message is being shown in this entre process } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) { for (Purchase purchase : purchases) { handlePurchase(purchase); } Toast.makeText(HelpActivity.this, "You already own the item", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) { Toast.makeText(HelpActivity.this, "BILLING_UNAVAILABLE", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ERROR) { Toast.makeText(HelpActivity.this, "ERROR", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.DEVELOPER_ERROR) { Toast.makeText(HelpActivity.this, "DEVELOPER_ERROR", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_NOT_OWNED) { Toast.makeText(HelpActivity.this, "ITEM_NOT_OWNED", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_UNAVAILABLE) { Toast.makeText(HelpActivity.this, "ITEM_UNAVAILABLE", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED) { Toast.makeText(HelpActivity.this, "FEATURE_NOT_SUPPORTED", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.NETWORK_ERROR) { Toast.makeText(HelpActivity.this, "FEATURE_NOT_SUPPORTED", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) { Toast.makeText(HelpActivity.this, "SERVICE_DISCONNECTED", Toast.LENGTH_SHORT).show(); } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) { Toast.makeText(HelpActivity.this, "SERVICE_UNAVAILABLE", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(HelpActivity.this, " " + billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show(); } } }; private MaterialButton removeAdsBtn; private MaterialButton restorePurchaseBtn; private ProductDetails productDetails; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_help_avtivity); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); // Set vertical orientation Window window = this.getWindow(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { window.setStatusBarColor(this.getResources().getColor(R.color.black)); } restorePurchaseBtn = findViewById(R.id.restoreAdsBtn); removeAdsBtn = findViewById(R.id.removeAdsBtn); // Initialize BillingClient setupBillingClient(); restorePurchaseBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { /* restorePurchases();*/ } }); removeAdsBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // Call the method to initiate the billing flow setupBillingClient(); initiateBillingFlow(); } }); } public void setupBillingClient() { billingClient = BillingClient.newBuilder(this).setListener(purchasesUpdatedListener).enablePendingPurchases().build(); establishConnection(); } void establishConnection() { billingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(@NonNull BillingResult billingResult) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { // The BillingClient is ready. You can query purchases here. QueryPurchase(); } } @Override public void onBillingServiceDisconnected() { // Try to restart the connection on the next request to // Google Play by calling the startConnection() method. establishConnection(); } }); } public void QueryPurchase() { QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(ImmutableList.of(QueryProductDetailsParams.Product.newBuilder().setProductId("removeads_rickroll").setProductType(BillingClient.ProductType.INAPP).build())).build(); billingClient.queryProductDetailsAsync(queryProductDetailsParams, new ProductDetailsResponseListener() { public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetailsList) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { // Check if the productDetailsList is not empty if (productDetailsList != null) { for (ProductDetails productDetails1 : productDetailsList) { productDetails = productDetails1; } } } } }); } public void initiateBillingFlow() { if (productDetails != null) { ImmutableList<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = ImmutableList.of( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) .build()); BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList(productDetailsParamsList) .build(); billingClient.launchBillingFlow(HelpActivity.this, billingFlowParams); } } private void handlePurchase(Purchase purchase) { ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build(); ConsumeResponseListener listener = new ConsumeResponseListener() { @Override public void onConsumeResponse(BillingResult billingResult, String purchaseToken) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { // Handle the success of the consume operation. } } }; if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { //Verify Purchase if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) { Toast.makeText(this, "Error:- Invalid Purchase", Toast.LENGTH_SHORT).show(); return; } if (!purchase.isAcknowledged()) { AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build(); billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener); // Grant the user access to remove ads // Save the purchase details to SharedPreferences to remember the purchase Toast.makeText(HelpActivity.this, "Thank you for the purchase", Toast.LENGTH_SHORT).show(); Toast.makeText(HelpActivity.this, "Restarting the app, Please wait ...", Toast.LENGTH_SHORT).show(); SharedPreferences preferences = getSharedPreferences("adremoveSP", MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean("ads_removed", true); editor.apply(); Intent intent = new Intent(HelpActivity.this, MainActivity.class); startActivity(intent); } else { Toast.makeText(this, "Already Purchased", Toast.LENGTH_SHORT).show(); SharedPreferences preferences = getSharedPreferences("adremoveSP", MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean("ads_removed", true); editor.apply(); Intent intent = new Intent(HelpActivity.this, MainActivity.class); startActivity(intent); } } else if (purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE) { Toast.makeText(this, "UNSPECIFIED_STATE", Toast.LENGTH_SHORT).show(); } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) { Toast.makeText(this, "PENDING", Toast.LENGTH_SHORT).show(); } billingClient.consumeAsync(consumeParams, listener); } private boolean verifyValidSignature(String originalJson, String signature) { try { String base64Key = "<MyBaseKey>"; return Verify.verifyPurchase(base64Key, originalJson, signature); } catch (IOException e) { return false; } } protected void onResume() { super.onResume(); billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build(), (billingResult, list) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { for (Purchase purchase : list) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged()) { handlePurchase(purchase); } } } }); } }
Verify.Java
public class Verify { private static final String TAG = "IABUtil/Security"; private static final String KEY_FACTORY_ALGORITHM = "RSA"; private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) throws IOException{ if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) || TextUtils.isEmpty(signature)){ return false; } PublicKey key = generatePublicKey(base64PublicKey); return verifyM(key,signedData,signature); } public static PublicKey generatePublicKey(String encodedPublicKey)throws IOException{ try { byte[] decodedKey = Base64.decode(encodedPublicKey,Base64.DEFAULT); KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); }catch (NoSuchAlgorithmException e){ throw new RuntimeException(e); } catch (InvalidKeySpecException e){ String msg = "Invalid Key Specification: "+ e; throw new IOException(msg); } } public static boolean verifyM(PublicKey publicKey,String signedData, String signature){ byte [] signatureBytes; try{ signatureBytes = Base64.decode(signature,Base64.DEFAULT); }catch (IllegalArgumentException e){ return false; } try { Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM); signatureAlgorithm.initVerify(publicKey); signatureAlgorithm.update(signedData.getBytes()); if (!signatureAlgorithm.verify(signatureBytes)){ return false; } return true; }catch (IllegalArgumentException e){ return false; }catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e){ return false; } } }
问题排查与解决方案
1. 移除重复的BillingClient初始化
在removeAdsBtn点击事件中,重复调用setupBillingClient()会创建新的BillingClient实例并断开旧连接,直接触发SERVICE_DISCONNECTED。修改点击事件:
removeAdsBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { initiateBillingFlow(); } });
2. 修正错误的Toast提示
NETWORK_ERROR分支错误显示了FEATURE_NOT_SUPPORTED,修正为:
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.NETWORK_ERROR) { Toast.makeText(HelpActivity.this, "NETWORK_ERROR", Toast.LENGTH_SHORT).show(); }
3. 删除不必要的消费操作
移除广告属于永久型内购,不需要调用consumeAsync(该方法仅适用于消耗型商品),删除handlePurchase()中的以下代码:
billingClient.consumeAsync(consumeParams, listener);
4. 更新签名验证算法
Google Play当前推荐使用SHA256withRSA替代SHA1withRSA,修改Verify类中的常量:
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
5. 优化连接重试逻辑
直接递归调用establishConnection()可能导致频繁重试,添加延迟机制:
@Override public void onBillingServiceDisconnected() { new Handler(Looper.getMainLooper()).postDelayed(() -> establishConnection(), 3000); }
6. 确认测试环境配置
- 使用Google Play Console中添加的测试账号进行测试
- 确保商品ID
removeads_rickroll已在后台正确配置并发布至测试轨道 - 检查设备上的Google Play服务是否为最新版本
内容的提问来源于stack exchange,提问作者sagar raval
相关产品推荐
相关产品推荐

