In App Billing - Rapid Device Orientation - causes crash (IllegalStateException)
Asked Answered
H

4

5

I implemented In-app Billing (v3) according to Android's Implementing In-app Billing guide.

All works fine, until I rotate the device, then immediately rotate it back to it's original orientation. Actually, sometimes it works, and sometimes it crashes with:

java.lang.IllegalStateException: IabHelper was disposed of, so it cannot be used.

Seems this is related to the asynchronous nature of IAB, though I'm not positive.

Any thoughts?

Hirza answered 14/8, 2013 at 4:18 Comment(0)
Q
6

You're probably getting the exception because somewhere in the activity lifecycle, you called mHelper.dispose(), then tried to use that same disposed instance later on. My recommendation is to only dispose of mHelper in onDestroy() and recreate it in onCreate().

However, you will run into another problem with IabHelper and device rotation. The problem goes like this: in your activity's onCreate(), you create the IabHelper instance mHelper and set it up. Later, you call mHelper.launchPurchaseFlow(...) and the IAB popup dialog appears floating above your activity. Then you rotate the device, and the IabHelper instance gets disposed of in onDestroy(...) then recreated in onCreate(...). The IAB dialog is still showing, you press the purchase button, and the purchase completes. onActivityResult() is then called on your activity, and you naturally call mHelper.handleActivityResult(...). The problem is, launchPurchaseFlow(...) has never been called on the recreated instance of IabHelper. IabHelper only handles the activity result in handleActivityResult(...) if launchPurchaseFlow(...) has been previously called on the current instance. Your OnIabPurchaseFinishedListener will never be called.

My solution to this was to modify IabHelper to allow you to tell it to expect handleActivityResult(...) without calling launchPurchaseFlow(...). I added the following to IabHelper.java

public void expectPurchaseFinished(int requestCode, OnIabPurchaseFinishedListener listener)
{
    mRequestCode = requestCode;
    mPurchaseListener = listener;
}

This will cause IabHelper to call onIabPurchaseFinished(...) on the listener when handleActivityResult(...) is called. Then, you do this:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) 
{
    mHelper.expectPurchaseFinished(requestCode, mPurchaseFinishedListener);
    mHelper.handleActivityResult(requestCode, resultCode, data);
}

My entire copy of IabHelper can be found here https://gist.github.com/benhirashima/7917645. Note that I updated my copy of IabHelper with the version found in this commit, which fixes a few bugs and has not been published in the Android SDK Manager. Also note that there are newer commits, but they contain new bugs and should not be used.

Quadruped answered 11/12, 2013 at 22:19 Comment(2)
Neat solution regarding handling the rotation event. I had some trouble understanding what expectPurchaseFinished implied, but I eventually realized that you're setting a new instance of purchaseFinishedListener just before handling the result, which guarantees that there is a listener to handle the purchasedFinished event even if the activity and iabHelper were recreated.Principled
We can extend the same idea to handle the mPurchasingItemType (only matters if you are using both subs and inapp, e.g. use onSaveInstanceState and onRestoreInstanceState, then set the correct item type if is not set).Principled
J
2

Here's what I did:

The code to instantiate the IabHelper and call startSetup() is within onCreate(), so it will be recreated when the device is rotated, so long as you aren't handling configuration changes on your own.

Also, be certain you are calling .handleActivityResult() at the beginning of onActivityResult(). This will ensure that your IabHelper reference is correctly cleaned up after the purchase dialog is closed.

With those two things in place, you shouldn't see any more crashes. But you will notice one more thing:

If you start the purchase dialog with a call to launchPurchaseFlow() and then rotate the device, the dialog will stay open, but now your Activity's IabHelper reference has been overwritten since onCreate() is called on device rotation. Because of this, when you close the dialog, the new IabHelper's handleActivityResult() method is called, but it doesn't match up with the requestCode you passed to launchPurchaseFlow() earlier, so your onPurchaseFinishedListener will not be notified. To handle this case (device rotations when the dialog is open), you'll need to handle the requestCode yourself inside of onActivityResult(). Since the dialog was closed, you'll want to mimic what you did inside your onPurchaseFinishedListener (discover if the user actually bought something). I just made a call to queryInventoryAsync() to find that out.

I'm not sure if that is the ideal solution, but it works well for me. I tried hanging on to the IabHelper reference like you did, but I saw weird issues where it would lose its setup state but wouldn't allow me to re-set it up.

A final thing I did was update billing util classes with the latest from the android source. There are some bug fixes that haven't been pushed to the SDK manager. Most of them are questionable null checks, but there are some improvements to prevent crashes:

latest changeset

Johnathon answered 28/9, 2013 at 7:19 Comment(0)
H
0

I tried to make mHelper static, and only instantiate it if (mHelper == null), and NOT destroy it in the activity's onDestroy() method. Also, pass the Application context to IabHelper. This way, once it's setup, it sticks around, and there's no need to worry about asynchronous operations (causes by device orientation) anymore.

Here's an outline of my code:

static IabHelper mHelper;
public void onCreate(Bundle savedInstanceState) {
    ...
    if (mHelper == null) {
        mHelper = new IabHelper(getApplicationContext(), base64EncodedPublicKey);
        mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
            ...
        });
    }
    ...
}

protected void onDestroy() {
    ...
    // Don't do ANYTHING to mHelper, so it will stick around on orientation change
}

Not sure if this is the right fix or not, but thought I'd mention it in case it help others.

Hirza answered 14/8, 2013 at 4:18 Comment(3)
I was thinking of doing the same thing but I don't know if there are unwanted side effects to this. Keeping a IabHelper means we keep a permanent service connection to InAppBillingService. Did you experience any problems with this?Preece
This is an old issue but... It is okay to pass in the Application context, If you look at the source they state that you can use an activity or application context code.google.com/p/marketbilling/source/browse/v3/src/com/…Vladamar
Isn't it basically a memory leak though, since static IabHelper will hold a reference to the Activity through the listener?Leniency
Q
0

After I tried many suggestions in SO which they didn't solve this issue, I tried this simple null checks which solved the problem:

First I checked if mHelper is null, then create a new instance:

In onCreate

if (mHelper == null) {
    mHelper = new IabHelper(this, base64EncodedPublicKey);
}

And I added other implementations inside a null check on mHelper again:

//noinspection ConstantConditions
if (mHelper != null){

    mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
        public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
        }
    };
    mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
        public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
        }
    };
    mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
        public void onIabSetupFinished(IabResult result) {
        }
    });
}

And of Course you should dispose helper:

@Override
public void onDestroy() {
    super.onDestroy();
    if (mHelper != null)
        mHelper.dispose();
    mHelper = null;
}

If the problem still resists, update your IabHelper class.

Quartz answered 5/9, 2017 at 13:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.