I am using the IabHelper
utility classes, as recommended by Google\'s tutorial, and I\'m being hit hard by this error. Apparently IabHelper
can not
A simple tricky solution
before calling purchaseItem method just add this line
if (billingHelper != null) billingHelper.flagEndAsync();
so your code looks this way
if (billingHelper != null) billingHelper.flagEndAsync();
purchaseItem("android.test.purchased");
Note: don't forget to make public flagEndAsync() method in IabHelper if you call it from another package.
This was not easy to crack but I found the needed workarounds. Quite disappointed with Google lately, their Android web sites have become a mess (very hard to find useful info) and their sample code is poor. When I was doing some Android development a few years ago it all went so much easier! This is yet another example of that...
Indeed IabUtil is buggy, it does not properly call off its own async tasks. The complete set of necessary workarounds to stabilise this thing:
1) make method flagEndAsync
public. It is there, just not visible.
2) have every listener call iabHelper.flagEndAsync
to make sure the procedure is marked finished properly; it seems to be needed in all listeners.
3) surround calls with a try/catch
to catch the IllegalStateException
which may occur, and handle it that way.
Really annoying issue. Here is a quick and dirty solution that is not perfect code wise, but that is user friendly and avoids bad ratings and crashes:
if (mHelper != null) {
try {
mHelper.launchPurchaseFlow(this, item, RC_REQUEST, mPurchaseFinishedListener, "");
}
catch(IllegalStateException ex){
Toast.makeText(this, "Please retry in a few seconds.", Toast.LENGTH_SHORT).show();
}
}
This way the user just has to tap another time (2 times at worst) and gets the billing popup
Hope it helps
Another major issue with the IabHelpr class is the poor choice of throwing RuntimeExcptions (IllegalStateException) in multiple methods. Throwing RuntimeExeptions from your own code in most cases is not desirable due to the fact that they are unchecked exceptions. That is like sabotaging your own application- if not caught, these exceptions will bubble up and crash your app.
The solution to this is to implement your own checked exception and change the IabHelper class to throw it, instead of the IllegalStateException. That will force you to handle this exception everywhere it could be thrown in your code at compile time.
Here is my custom exception:
public class MyIllegalStateException extends Exception {
private static final long serialVersionUID = 1L;
//Parameterless Constructor
public MyIllegalStateException() {}
//Constructor that accepts a message
public MyIllegalStateException(String message)
{
super(message);
}
}
Once we make the changes in the IabHelper class, we can handle our checked exception in our code where we call the class methods. For example:
try {
setUpBilling(targetActivityInstance.allData.getAll());
} catch (MyIllegalStateException ex) {
ex.printStackTrace();
}
A little-modified version of NadtheVlad's answer that works like charm
private void makePurchase() {
if (mHelper != null) {
try {
mHelper.launchPurchaseFlow(getActivity(), ITEM_SKU, 10001, mPurchaseFinishedListener, "");
}
catch(IllegalStateException ex){
mHelper.flagEndAsync();
makePurchase();
}
}
}
The logic is simple, just put the launchPurchaseFlow()
thing in a method and use recursion in the catch block. You still need to make flagEndAsync()
public from the IabHelper
class.
This answer directly addresses the problem that @Wouter has seen...
It is true that onActivityResult()
must be triggered, like many people have said. However, the bug is that Google's code isn't triggering onActivityResult()
in certain circumstances, i.e. when you're pressing your [BUY] button twice when running the debug build of your app.
Additionally, one major problem is that the user may be in a shaky environment (i.e. Bus or subway) and presses your [BUY] button twice... suddenly you've got yourself an exception !
At least Google fixed this embarrassing exception https://github.com/googlesamples/android-play-billing/commit/07b085b32a62c7981e5f3581fd743e30b9adb4ed#diff-b43848e47f8a93bca77e5ce95b1c2d66
Below is what I implemented in the same class where IabHelper
is instantiated (for me, this is in the Application class) :
/**
* invokes the startIntentSenderForResult - which will call your activity's onActivityResult() when it's finished
* NOTE: you need to override onActivityResult() in your activity.
* NOTE2: check IAB code updates at https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main/java/com/example/android/trivialdrivesample/util
* @param activity
* @param sku
*/
protected boolean launchPurchaseWorkflow(Activity activity, String sku)
{
if (mIabIsInitialized)
{
try
{
mHelper.launchPurchaseFlow(
activity,
sku,
Constants.PURCHASE_REQUEST_ID++,// just needs to be a positive number and unique
mPurchaseFinishedListener,
Constants.DEVELOPER_PAYLOAD);
return true;//success
}
catch (IllegalStateException e)
{
mHelper.flagEndAsync();
return launchPurchaseWorkflow(activity, sku);//recursive call
}
}
else
{
return false;//failure - not initialized
}
}
My [BUY] button calls this launchPurchaseWorkflow()
and passes the SKU and the activity the button is in (or if you're in a fragment, the enclosing activity)
NOTE: be sure to make IabHelper.flagEndAsync()
public.
Hopefully, Google will improve this code in the near future; this problem is about 3 years old and it's still an ongoing problem :(