Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android In-App Billing v3: "Can't perform operation: queryInventory"

I have setup In-App Billing for the first time using the new v3 API. It is working correctly on my devices but I have received a lot of error reports from other users.

One of them is:

java.lang.IllegalStateException: IAB helper is not set up. Can't perform operation: queryInventory
    at my.package.util.iab.IabHelper.checkSetupDone(IabHelper.java:673)
    at my.package.util.iab.IabHelper.queryInventory(IabHelper.java:462)
    at my.package.util.iab.IabHelper$2.run(IabHelper.java:521)
    at java.lang.Thread.run(Thread.java:1019)

And another one is:

java.lang.NullPointerException
    at my.package.activities.MainActivity$4.onIabSetupFinished(MainActivity.java:159)
    at my.package.util.iab.IabHelper$1.onServiceConnected(IabHelper.java:242)

My activity implementation follows Google's example code (all referenced classes are untouched from the example):

IabHelper mHelper;

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //...

    mHelper = new IabHelper(this, IAB_PUBLIC_KEY);
    mHelper.enableDebugLogging(true);

    mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
        public void onIabSetupFinished(IabResult result) {
            if (!result.isSuccess()) {
                // Oh noes, there was a problem.
                return;
            }

            // Hooray, IAB is fully set up. Now, let's get an inventory of
            // stuff we own.
            mHelper.queryInventoryAsync(mGotInventoryListener); //***(1)***
        }
    });
}

// Listener that's called when we finish querying the items we own
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
    public void onQueryInventoryFinished(IabResult result,
            Inventory inventory) {
        if (!result.isFailure()) {
            if (inventory.hasPurchase(SoundsGlobals.IAB_SKU_PREMIUM)){
                //we are premium, do things
            }
        }
        else{
            //oops
        }
    }
};

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

I assume that both errors originate from the line marked as ***(1)***

What is the cause of these errors? If I'm calling queryInventoryAsync only within onIabSetupFinished, how is it possible that mHelper is null, or that mHelper is not set up?

Does anyone know a solution to this?

like image 943
Ereza Avatar asked Dec 25 '12 00:12

Ereza


5 Answers

As @Martin explained, there was a bug in the Google In-App Billing example which caused this.

However, after fixing it, I was still receiving some errors in internal calls (queryInventory inside the thread created in queryInventoryAsync in some rare cases reports that the helper is not setup). I have solved this by adding an additional catch in that case:

try {
    inv = queryInventory(querySkuDetails, moreSkus);
}
catch (IabException ex) {
    result = ex.getResult();
}
catch(IllegalStateException ex){ //ADDED THIS CATCH
    result = new IabResult(BILLING_RESPONSE_RESULT_ERROR, "Helper is not setup.");
}

I also got a crash on mHelper.dispose() which I fixed in a similar way:

try{
    if (mContext != null) mContext.unbindService(mServiceConn);
}
catch(IllegalArgumentException ex){ //ADDED THIS CATCH
    //IGNORE IT - somehow, the service was already unregistered
}

Of course, instead of ignoring these errors you can silently log them to ACRA, for example :)

Thanks for all your comments.

like image 98
Ereza Avatar answered Nov 05 '22 04:11

Ereza


There is a bug in IABHelper. The return line in the exception handler is missing, meaning it drops through and calls the success hanlder - however, mSetupDone has not been set, so further calls to the API fail. Add the return statement in as below - this will still fail, but the failure will be correctly reported to your app so you can take appropriate action.

                catch (RemoteException e) {
                if (listener != null) {
                    listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
                                                "RemoteException while setting up in-app billing."));
                }
                e.printStackTrace();
                return;  // This return line is missing
            }

            if (listener != null) {
                listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
            }
like image 14
Martin Avatar answered Nov 05 '22 02:11

Martin


I believe that there are still two bugs in the Android code, which explains why you still see the error. Notice that the call stack is on a stand-alone thread. But the code that sets mSetupDone (IabHelper) to true executes on the main UI thread. Java does not guarantee that data changed by one thread will be visible to the other thread due to CPU caching unless you declare the variable with the volatile keyword. Thus it is possible that it was setup (mSetupDone == true), but that the new value of mSetupDone is cached on UI thread, not yet visible to this thread in your call stack. So that thread still sees mSetupDone == false.

I tried to fix this by declaring mSetupDone with volatile, and also every other non-final field of IabHelper just to be safe.

Now the other problem is the .dispose() function. It does not stop the ongoing threads. This means that it can set mSetupDone to false while one of the worker threads is running. If you look at queryInventoryAsync(), you will see it does check that mSetupDone is true. And based on your call stack, it did get past that. Then it crashed later with mSetupDone == false. Only way that could happen is if dispose() were called while your thread was in flight. Fix is that dispose() needs to signal threads to just silently bail out instead of continuing and throwing errors when it sees mSetupDone == false. This also prevents yet another problem with IabHelper where disposed instances call listener callbacks even after being disposed! It's a bit complicated to explain line by line here, but hopefully thus gets you pointed in the right direction.

like image 10
David M Avatar answered Nov 05 '22 02:11

David M


I found out! It's about the version of the user's Google Play Store app. In-app billing V3 needs 3.9.16 or higher (http://developer.android.com/google/play/billing/versions.html). I used an older version and I received that error, now on 4.4.21 its ok!

like image 6
edwell Avatar answered Nov 05 '22 02:11

edwell


Make sure you are implementing the IabHelper.han.handleActivityResult(requestCode, resultCode, data) method in your activities onActivityResult method.

   @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {

// Pass on the activity result to the helper for handling
  if (!mIabHelper.handleActivityResult(requestCode, resultCode, data)) {
      // not handled, so handle it ourselves (here's where you'd
      // perform any handling of activity results not related to in-app
      // billing...
      super.onActivityResult(requestCode, resultCode, data);
  } else {
      Log.i(TAG, "onActivityResult handled by IABUtil.");
  }
}
like image 2
KevinM Avatar answered Nov 05 '22 02:11

KevinM