I'm using google IAP billing library 5, and after more or less directly copying the code from developer.android.com I'm getting a SERVICE_DISCONNECTED error whenever I try to consume a INAPP purchase that was formally PENDING and has just become CHARGEABLE. While it's plausible that the server has indeed disconnected, this is only 1-2 minutes after making the (test) charge, and my onBillingServiceDisconnected() listener does not get called until much, much later. So I'm wondering if it's a false alarm and something else is really the problem. Indeed, if I then force a re-connection it (falsely) reports there are no CHARGEABLE purchases; if however I quit the app completely and come back I do detect the CHARGEABLE purchase and am able to successfully consume it.
(Note, test charges that don't go thru a PENDING state get consumed with zero problems every time).
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
}
So this turned out to be a combination of issues:
onPurchasesUpdated is called by the billing API inside a try/catch that silently eats errors(come on google, at least log.e them!). So my code was causing exceptions that I never saw. Solution: wrap your function inside another try/catch so that you can at least TOAST the errors or you'll never know what and why things are failing! Once I figured this out, it was mostly easy to figure out the rest.
I was updating the UI from my code to trace the order of what happened with something less easy to miss than a toast. But in fact any attempt to touch the UI caused an exception because the listener doesn't get called in the UI thread. So you need to use a runnable, like this:
public String headerTxt(String str) { runOnUiThread(new Runnable() { @Override public void run() { header.setText(str); }}); return str; }
the play store status does not update very quickly; ie a purchase will show as pending or consumable way after you have successfully consumed a pending purchase. As much as 4 hours can pass before the console shows the true status. Makes debugging very tricky. Hopefully they will make it closer to real time someday.
So the working version of the code (using the headerTxt function defined above) looks quite similar to the original code.
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED)
{toastUI("Purchase Canceled"); return;}
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK || purchases == null)
{toastUI("Error: " + billingResult.toString()); return; }
// only option left is BillingClient.BillingResponseCode.OK
for (Purchase purchase : purchases)
{
if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
Log.e(TAG, headerTxt("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)
{
Log.e(TAG, headerTxt("Purchase Complete: " + purchase.getProducts()) );
MainActivity.settings.edit().putString("IAP", purchase.getProducts().get(0).toString()).commit();
}
else if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
{
Log.e(TAG, headerTxt("Server disconnected, retrying " + billingResult1.toString()) );
billingClient.startConnection(IAP_Activity.this);
}
else
Log.e(TAG, headerTxt("purchase consumption failed: \n " + billingResult1.getDebugMessage()) );
};
billingClient.consumeAsync(consumeParams, listener);
}
}
}