7

I'm attempting to make a free app upgradable to the "paid" version using in-app billing. I used code from this tutorial to handle the billing as the one on the official developer site is too complex and messy to follow for a simple flow like mine.

My code to do the upgrade works fine.

The problem comes when I try to add something to check if the user has already purchased but has lost their purchase data either by reinstalling or by clearing the data (I don't care which).

On app startup I check for a flag that's set after the first run. If that flag isn't there, the user is shown a dialog warning them that the app will check for previous purchases, and when they click ok, the restoreTransactionInformation method is called. This then causes the application to force close.

Because in-app billing doesn't work when debugging or on the emulator, I have to publish a signed version of the app each time I want to try the code. I have no way of knowing why the application exits when I try to make the restoreTransactionInformation request. Does anyone have a clue how I can diagnose it, or what might be causing my app to die? Or a working example of how to use the restoreTransactionInformation method?

EDIT: So it looks like the RESTORE_TRANSACTIONS request is getting a correct response, and returning details of my test purchase. Unfortunately before it can do anything with it, the app is forced closed. Here's a logcat (without obfuscated code) of what happens right after market responds to the RESTORE_TRANSACTIONS request:

I/BillingService( 6484): confirmTransaction()
D/Finsky  ( 1884): [7] MarketBillingService.getPreferredAccount: com.hippypkg: Account from first account.
I/BillingService( 6484): current request is:**********
I/BillingService( 6484): RESTORE_TRANSACTIONS Sync Response code: RESULT_OK
D/WindowManagerImpl( 6484): finishRemoveViewLocked, mViews[0]: com.android.internal.policy.impl.PhoneWindow$DecorView@**********
W/InputManagerService( 1381): [unbindCurrentClientLocked] Disable input method client.
W/InputManagerService( 1381): [startInputLocked] Enable input method client.
D/NativeCrypto( 1884): returned from sslSelect() with result 1, error code 2
D/Finsky  ( 1884): [1] MarketBillingService.sendResponseCode: Sending response RESULT_OK for request ********** to com.hippypkg.
I/BillingService( 6484): Received action: com.android.vending.billing.PURCHASE_STATE_CHANGED
I/BillingService( 6484): purchaseStateChanged got signedData: {"nonce":**********,"orders":[{"orderId":"**********","packageName":"com.hippypkg","productId":"hippy_upgrade_free_to_full","purchaseTime":1331476540000,"purchaseState":0}]}
I/BillingService( 6484): purchaseStateChanged got signature: **********==
I/BillingService( 6484): signedData: {"nonce":**********,"orders":[{"orderId":"**********","packageName":"com.hippypkg","productId":"hippy_upgrade_free_to_full","purchaseTime":1331476540000,"purchaseState":0}]}
I/BillingService( 6484): signature: **********==
I/BillingService( 6484): confirmTransaction()
I/BillingService( 6484): makerequestbundle success
I/BillingService( 6484): putstringarray success
D/Finsky  ( 1884): [24] MarketBillingService.getPreferredAccount: com.hippypkg: Account from first account.
D/AndroidRuntime( 6484): Shutting down VM
W/dalvikvm( 6484): threadid=1: thread exiting with uncaught exception (group=0x4001d5a0)
E/AndroidRuntime( 6484): FATAL EXCEPTION: main
E/AndroidRuntime( 6484): java.lang.RuntimeException: Unable to start receiver com.hippypkg.BillingReceiver: java.lang.NullPointerException
E/AndroidRuntime( 6484):    at android.app.ActivityThread.handleReceiver(ActivityThread.java:2144)
E/AndroidRuntime( 6484):    at android.app.ActivityThread.access$2400(ActivityThread.java:135)
E/AndroidRuntime( 6484):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1114)
E/AndroidRuntime( 6484):    at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime( 6484):    at android.os.Looper.loop(Looper.java:150)
E/AndroidRuntime( 6484):    at android.app.ActivityThread.main(ActivityThread.java:4385)
E/AndroidRuntime( 6484):    at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime( 6484):    at java.lang.reflect.Method.invoke(Method.java:507)
E/AndroidRuntime( 6484):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:849)
E/AndroidRuntime( 6484):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:607)
E/AndroidRuntime( 6484):    at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime( 6484): Caused by: java.lang.NullPointerException
E/AndroidRuntime( 6484):    at android.os.Parcel.readException(Parcel.java:1328)
E/AndroidRuntime( 6484):    at android.os.Parcel.readException(Parcel.java:1276)
E/AndroidRuntime( 6484):    at com.android.vending.billing.IMarketBillingService$Stub$Proxy.sendBillingRequest(IMarketBillingService.java:100)
E/AndroidRuntime( 6484):    at com.hippypkg.BillingHelper.confirmTransaction(BillingHelper.java:152)
E/AndroidRuntime( 6484):    at com.hippypkg.BillingHelper.verifyPurchase(BillingHelper.java:250)
E/AndroidRuntime( 6484):    at com.hippypkg.BillingReceiver.purchaseStateChanged(BillingReceiver.java:41)
E/AndroidRuntime( 6484):    at com.hippypkg.BillingReceiver.onReceive(BillingReceiver.java:23)
E/AndroidRuntime( 6484):    at android.app.ActivityThread.handleReceiver(ActivityThread.java:2103)
E/AndroidRuntime( 6484):    ... 10 more
W/ActivityManager( 1381):   Force finishing activity com.hippypkg/.Hippy
Hippyjim
  • 2,520
  • 6
  • 38
  • 54
  • Hey there; Did you find a solution yet? I just came across this issue and am stuck at the exact same place as you. – Sid Apr 12 '12 at 02:47
  • Not so far - unfortunately I got moved to working on something else and haven't had chance to look. Follow the thread on the response by Nikolay for the best result I had so far. Let me know if you get anywhere! – Hippyjim Apr 16 '12 at 09:29
  • Solved it, 4 hours and 5 coffees later :) Check out the answer. – Sid Apr 16 '12 at 18:44
  • @Hippyjim can u show? how u write ur code for restore transaction..actally i am facing the same problem – AndroidDev Dec 03 '12 at 09:36

3 Answers3

4

So I finally managed to figure it out.

If you look at the Google Docs for the In App Billing Overview, it states that:

The RESTORE_TRANSACTIONS request type also triggers a PURCHASE_STATE_CHANGED broadcast intent, which contains the same type of transaction information that is sent during a purchase request, although you do not need to respond to this intent with a CONFIRM_NOTIFICATIONS message.

In a normal purchase-confirmTransaction cycle, when you request to purchase an In App Billing product, google sends back a JSON with a bunch of fields. One of these fields is 'notification_id'. When google sends a PURCHASE_STATE_CHANGED intent, it expects a CONFIRM_NOTIFICATIONS response from the app, with a bundle containing a bunch of info, including the notification_id's. All well and good here.

The problem starts when you get a PURCHASE_STATE_CHANGED from Google for a RESTORE_TRANSACTIONS request from the app. This JSON does not contain notificaion_id fields. The library still responds with a CONFIRM_NOTIFICATIONS, adding the notification_id array to the bundle, which, in this case, is null. That's what causes the NullPointerException.

Solution: I modified the BillingHelper.java class by adding a boolean to track when the user makes a normal purchase, and when he wants to restoreTransactions. If it's a restoreTransactions request, I send a message back to the handler and skip the confirmNotifications step.

EDIT: The code for the above fix is in BillingHelper.java I am using a boolean flag to track whether the user made a RESTORE_TRANSACTIONS call (isRestoreTransactions).

In BillingHelper.java's 'verifyPurchase' method, I changed the code as follows:

protected static void verifyPurchase(String signedData, String signature) {
        ArrayList<VerifiedPurchase> purchases = BillingSecurity.verifyPurchase(signedData, signature);

        if(isRestoreTransaction)
        {
            /*
            *
            *Add some logic to retrieve the restored purchase product ID's from the 'purchases' array
            *
            */

            //Set the boolean to false
            isRestoreTranscation = false;

            //Send a message to the handler, informing it that purchases were restored
            if(mCompletedHandler != null){
                mCompletedHandler.sendEmptyMessage(0);
            } else {
                Log.e(TAG, "verifyPurchase error. Handler not instantiated. Have you called setCompletedHandler()?");
            }
        }
        else
        {
            /*
            *......
            *......
            *......
            *Original method body here
            *......
            *......
            *......
            */
        }
    }
Sid
  • 9,508
  • 5
  • 39
  • 60
  • Sid, thanks so much - that cracked it. As there's only 1 purchase possible from my app, rather than mess around with booleans etc, I simply check if the latestPurchase.notificationId was null. If so, it doesn't try to send the confirmation (if it's null it won't work). If your solution is a little more comprehensive, it might be good for future visitors if you could post the code changes here, or perhaps to the tutorial we got this from. Thanks again! – Hippyjim Apr 20 '12 at 23:47
  • can you give me that changes that you made for the RESTORE_TRANSACTIONS...? I am also stuck with that problem for few days. please help me by providing the changes that you have made. – Rushabh Patel Sep 06 '12 at 14:18
  • hi Sid, i have tried your solution but it still crashes. restore does not recieve any data i have put one hello toast but still is not displaying and suddenly it crashes. – Rushabh Patel Sep 07 '12 at 07:53
  • Rushabh, are you sure that your crash log in logcat matches that in the question? If you're not receiving a response in the first place, seems like your issue lies somewhere else. It would be a good idea to post a detailed question with the code and crash log, instead of discussing it in the comments :) – Sid Sep 07 '12 at 09:09
4

You can't really make a distinction between 'reinstalling' and 'cleared app data'. They are essentially the same: shared preferences are empty. Nor should you need to.

As for diagnosing the issue, put a 'restore transactions' button and simply click it in different states (just installed, flag(s) set, etc.). Then watch the logcat.

BTW, it might be better to stick with the original Google code at first, you would get more help that way. There also some projects on Google Code that wrap the IAB code to make it a bit easier to integrate.

Nikolay Elenkov
  • 52,576
  • 10
  • 84
  • 84
  • Thanks, I've clarified the question as I don't really care how they lost their data - I know it's not possible to tell the difference between a new install and one that had data wiped. As I mentioned, to be able to use billing to check for managed in-app purchases, you need to use a signed app not in debug mode, so it has to run on a real device. That means I have no logcat - unless I'm missing something? As for using the enormous Google example, it's just too complex and I can't work out how to make my simple purchase work at all in it, hence using the wrapper I mentioned. – Hippyjim Mar 19 '12 at 15:59
  • You seem to be missing something :) You do have logcat, you can also debug the app if you set the debuggable flag in the manifest. Using the 'enormous' example has the advantage that you'll actually need to understand how it works internally, and that will help you fix when it's broken. You should at least read the part on testing: http://developer.android.com/guide/market/billing/billing_testing.html – Nikolay Elenkov Mar 19 '12 at 16:16
  • Thanks, unfortunately I've read that page and it's not much help. It only talks about the actual purchase, which I did with no problem. When trying to look at existing purchases (making a RESTORE_TRANSACTIONS request), I get the RESULT_DEVELOPER_ERROR for every request if the app is in debug mode. According to the same guide you just referred me to, that's expected behaviour. So the only way to test, is with a signed application - with no debug. Is there a way to see the logcat on a live device? – Hippyjim Mar 19 '12 at 16:54
  • Also, using the "enormous" example, would be like trying to learn to drive, by building a car. It doesn't actually contain much of what I need to make this work, and each piece of code is so buried in "helpers" I had about 10 files open just to find out what one method did...only to find out it was irrelevant. – Hippyjim Mar 19 '12 at 16:56
  • You are confusing the two things: the application being signed with the released key has nothing to do with debugging/logging. Plug a USB cable, run `adb logcat` in a terminal and you'll get the logs. Open the Eclipse DDMS perspective, attach to the app app process and you can debug too. – Nikolay Elenkov Mar 20 '12 at 02:13
  • Ah ok, I didn't realise I was able to look at logs like that. I'll check it out - thanks! – Hippyjim Mar 20 '12 at 12:34
  • Thanks to Nikolay I was able to see a real logcat - unfortunately I'm still struggling to see why it's dying. it all seems to have received the response ok, but then claims not to be able to start the receive, even though it's just logged that it's received the response. I've added the logcat to the original question above - any ideas? – Hippyjim Mar 23 '12 at 00:43
  • You have some obfuscated code, so it's not too clear what's going on, but it looks similar to this: http://code.google.com/p/marketbilling/issues/detail?id=25 Open the Android Market/Play app, and accept any agreements that might pop up. Also try it with non-obfuscated code for easier debugging. – Nikolay Elenkov Mar 23 '12 at 01:01
  • Thanks for sticking with this Nikolay - it's appreciated. I've updated the logcat above without the obfuscation - it looks like the app is dying just at the point where it's making the IMarketBillingService.sendBillingRequest call - presumably I'm passing something in that I shouldn't. I didn't have any EULA prompt or anythign when opening "Play Shop" (the new name for market) - so O guess we can rule that out. – Hippyjim Mar 23 '12 at 01:26
  • Looks like some sort of bug in the 'Play Shop/Store' to me, but set a breakpoint on `BillingHelper.java:152` and check what you are passing. Should be the signature and the transaction data (signedData, JSON string). – Nikolay Elenkov Mar 23 '12 at 01:56
  • Thanks Nikolay, you were half right. not passing the JSON was the problem, but because of the way that Play/Market responded to the restore transaction request. Either way, we wouldn't have got there without your input. – Hippyjim Apr 20 '12 at 23:49
0

I'm also developing a free app upgradable, but I used the oficial sample instead of Blundell's tutorial, because that tutorial does not save information and doesnt use Managed items

Just take a look on the restoreDatabase() method in the Dungeons sample, it does what you want, checks, using the SharedPreferences if it's the first run and if it is calls the restoreTransactions method.

To debug, just connect your device to eclipse and check the logcat, just dont forget to set the constant Debug (in Consts.java) to true and in the manifest set the debuggable tag to true also.

To understand the sample code better I just added a lot more debug and now its working.

ricvieira
  • 1,053
  • 7
  • 8