Implementing In-App Billing for Android

Although my wallpaper is provided free of charge in the Google Play Store, it comes as a demo version. Some important features are disabled by default and the user needs to buy an in-app product to unlock these features.

Google provides an easy to use library that is able to handle the payment and querying tasks and shields away lots of the complexity behind it. In this article I present how I implemented the In-App billing functionality for my Live Wallpaper and how I solved the problems that I faced.

To be able to offer an In-App product, it has to be configured within the Play Store:

Configuration of In-App Items

The Buy-Activity

To give the user a chance to buy the in-app product, I created a BuyPremiumActivity. This Activity shows a text that explains what advantages the premium version has, the price of the app and a button to actually buy it:

Buy Premium Activity

Initializing the In-App Billing API

The first task when starting this Activity is to query the Billing API to return the price for the app. This is done while initializing the API:

[java title=“Getting the Price for the In-App SKU“]
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

[…]

// Initially, disable the "buy me" button
butBuy.setEnabled(false);

buyHelper = new IabHelper(this, PUBLIC_KEY);
buyHelper.startSetup(new OnIabSetupFinishedListener() {
@Override
public void onIabSetupFinished(IabResult result) {
if(result.isSuccess()) {
// Fill a list of SKUs that we want the price infos for
// (SKU = "stockable unit" = buyable things)
ArrayList<String> moreSkus = new ArrayList<String>();
moreSkus.add(SKU_NAME_PREMIUM);

// We initialize the price field with a "retrieving price" message while we wait
// for the price
final TextView tvPrice = (TextView)BuyPremiumActivity.this.findViewById(R.id.price);
tvPrice.setText(R.string.waiting_for_price);

// Start the query for the details for the SKUs. This runs asynchronously, so
// it may be that the price appears a bit later after the rest of the Activity is shown.
buyHelper.queryInventoryAsync(true, moreSkus, new QueryInventoryFinishedListener() {
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inv) {
if(result.isSuccess()) {
// If we successfully got the price, show it in the text field
SkuDetails details = inv.getSkuDetails(Billing.SKU_PREMIUM);
String price = details.getPrice();

tvPrice.setText(price);

// On successful init and price getting, enable the "buy me" button
butBuy.setEnabled(true);
} else {
// Error getting the price… show a sorry text in the price field now
tvPrice.setText(R.string.cant_get_prices);
}
}
});
} else {
// If the billing API could not be initialized at all, show a sorry dialog. This
// will surely prevent the user from being able to buy anything.
Billing.showSorry(BuyPremiumActivity.this, R.string.cant_init_billing_api);
}
}
});

[…]
}
[/java]

The PUBLIC_KEY constant contains the public key for your App you can find in the Google Play Developer Console. Google suggest to split this string, rearrange and recombine it again during runtime to not make it too easy for hackers to extract out your key from just getting the strings out of your APK file.

The SKU_NAME_PREMIUM contains the id that you gave your In-App item in the Google Play Developer Console. But when testing your In-App Payment, you should use the String „android.test.purchased“ (or another one of the available static test-SKUs). In these cases, you will always get a predefined response when you buy something in your app. This is really useful when testing your Payment implementation. But note, that later, when you query the products a customer owns, this test flag will not stay for long. I encountered, that after some time this gets reset and the app will think the item has never been bought.

Integrating the In-App Library into the project

The IabHelper class comes from the Google In-App Billing sample project. Together with all the other classes in the same package, it is a powerful helper library when working with In-App Payment. The sample project can be downloaded using the SDK Manager is located in the directory Android-SDK/extras/google/play_billing/samples/TrivialDrive.

When integrating this library into you project, make sure you…

  • Copy the file Android-SDK/extras/google/play_billing/IInAppBillingService.aidl using the exact same package to your project
  • Copy all the classes under Android-SDK/extras/google/play_billing/samples/TrivialDrive/src/com/example/android/trivialdrivesample/util/* – but here you are free to change the package names.

Of course you need to add the BILLING right to your AndroidManifest.xml file to enable the billing:

[xml]
<uses-permission android:name="com.android.vending.BILLING" />
[/xml]

Testing Payments

If you are intensive testing your In-App payment, you will want to do the payment lots of times in a row while fixing bugs – no time to wait until the flag gets reset automatically. To be able to buy the in-app item again, you have to consume it. To do so, use the following code:

[java title=“consume an In-App item to be able to buy it again“]
Purchase pp = null;
try {
pp = new Purchase("inapp", "{\"packageName\":\"PACKAGE_NAME\","+
"\"orderId\":\"transactionId.android.test.purchased\","+
"\"productId\":\"android.test.purchased\",\"developerPayload\":\"\",\"purchaseTime\":0,"+
"\"purchaseState\":0,\"purchaseToken\":\"inapp:PACKAGE_NAME:android.test.purchased\"}",
"");
} catch (JSONException e) {
e.printStackTrace();
}

buyHelper.consumeAsync(pp, new OnConsumeFinishedListener() {
@Override
public void onConsumeFinished(Purchase purchase, IabResult result) {
if(result.isSuccess()) {
Toast.makeText(BuyPremiumActivity.this, "Purchase consumed!",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(BuyPremiumActivity.this, "Error consuming: "+result.getMessage(),
Toast.LENGTH_SHORT).show();
}
}
});
[/java]

Replace PACKAGE_NAME in the code above with the package name of your app.

I placed another button next to the „buy me“ with this code to easily do a buy / consume / buy / consume flow while testing the payment implementation.

The Purchase Flow

This is how I implemented the buying process:

[java title=“Buying an In-App Item“]
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

[…]

butBuy.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// Disable the button after the click to prevent double clicks
butBuy.setEnabled(false);

// Start the purchase flow
buyHelper.launchPurchaseFlow(BuyPremiumActivity.this,
SKU_NAME_PREMIUM, Billing.BUY_REQUEST_CODE,
new OnIabPurchaseFinishedListener() {
@Override
public void onIabPurchaseFinished(IabResult result, Purchase info) {
if (result.isSuccess()) {
// Successful – the item has been payed for

// We set a vale in the shared preferences to mark this app as
// being the premium version
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(BuyPremiumActivity.this);
prefs.edit()
.putBoolean(
PremiumHandler.KEY_PREMIUM_VERSION,
true).apply();

// Start the Settings Activity together withe the info, that
// the user just bought this app – in this case we show a
// "thank you" Dialog.
//
// The flag "CLEAR_TASK" is important, so the user is not sent
// back to this buy activity when he presses the back button.
//
Intent intent = new Intent(
BuyPremiumActivity.this,
SettingsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Billing.KEY_FIRST_BUY, true);
startActivity(intent);
}
}
});
}
});
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Delegate the answer from the purchase request to the buying helper – necessary
// to let it handle the answers from the backend
buyHelper.handleActivityResult(requestCode, resultCode, data);
}
[/java]

Don’t forget to cleanup after ourselves when the BuyActivity ends:

[java]
@Override
protected void onDestroy() {
super.onDestroy();
buyHelper.dispose();
}
[/java]

Check if the user has bought the In-App Item

So far, this offered a way for the user to see the price and buy our In-App item. Now I have to make sure, that the customer gets a value for his purchase. I have to determine at each start of the application, whether the customer owns the In-App item to decide to run the demo or the premium version of the app.

On a successful purchase flow, I save a flag „premium = true“ to the shared preferences to indicate, that the user owns the app. While this may seem sufficient at first, it has several drawbacks:

  • A rooted user can just change the xml file containing the shared preferences data and so switch the app to think its a premium version without paying for it.
  • When a customer switches his phone or reinstalls the app after deleting it, he has lost his premium status although Google still thinks the customer still owns it and refuses a re-buying of the item.

To circumvent these drawbacks, I have to check with Google Play Store each time the app starts whether the customer owns the In-App Item that indicates the premium version of the app. This check is done asynchronously again – it may take up to a second until we get a response. I have to wait for the results before I can show the final User Interface, because which interface is shown depends on the result of the check.

To not block the UI while waiting for the results, I places another Activity in front of all the others. This Activity starts the In-App product check and on its completion, launches the demo or the premium Activity, depending on the results.

After the check is completed, I update the shared preferences value to reflect the correct premium/demo state. The reason is: All other parts of the app can now just use this flag and do not need to bother with the payment API.

[java title=“Check for customer owned In-App products and delegate to appropriate Activity“]
private void checkForPremium() {
final IabHelper buyHelper = new IabHelper(this, Billing.pubKey);

// Default is false for premium version
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.edit()
.putBoolean(PremiumHandler.KEY_PREMIUM_VERSION, false)
.apply();

// initialize the InApp Billing system
buyHelper.startSetup(new OnIabSetupFinishedListener() {
@Override
public void onIabSetupFinished(IabResult result) {
if (!result.isSuccess()) {
// On error, say sorry and leave the app in demo mode
Billing.showSorry(SettingsActivity.this, R.string.sorry_billing_connect,
result.getMessage());
buyHelper.dispose();
return;
}

// Get a list of all products the user owns
buyHelper.queryInventoryAsync(new QueryInventoryFinishedListener() {
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inv) {
if (result.isFailure()) {
// If we could not get a list of the owned SKUs, say sorry and keep the demo version
Billing.showSorry(SettingsActivity.this,
R.string.sorry_getting_owned_products, result.getMessage());
buyHelper.dispose();
return;
} else {
boolean isPremium = inv.hasPurchase(SKU_NAME_PREMIUM);

// Set the shared preferences to premium=true if the user owns it
SharedPreferences prefs = PreferenceManager.
getDefaultSharedPreferences(SettingsActivity.this);
prefs.edit()
.putBoolean(PremiumHandler.KEY_PREMIUM_VERSION, isPremium)
.apply();

buyHelper.dispose();

// Forward to the currect activity depending on premium / demo mode
if (isPremium) {
// TODO start premium activity
} else {
// TODO start demo activity
}
}
}
});
}
});
[/java]

Now all the necessary elements for the In-App payment are implemented. Users can buy the In-App Item that represents the premium version of the app. On every start of the app (in my case: the Settings Dialog), Google is asked for the products the user owns and the premium state of the app is set accordingly. The state is copied over to a shared preferences entry to let all other parts of the App access this information easily.

Should the customer decide to give back his purchase, the ownership of the customer for the InApp item will vanish, and so the premium state of the app on its next launch.

I hope this post helped you in implementing your own In-App Billing implementation. If you have any questions or comments, I’d be glad if you’d share this article or leave a message in the comments section below.

Here is a small working example App that shows it in action: Download

28 comments

    • Stefan says:

      Hi Eyvind!

      I intentionally did not try the approach of a free and full version, because of these reasons:

      • Having In-App payment frees me from the additional effort to support two separate Apps.
      • The resistance to buy an app by just clicking a „Buy me“ button is lower than having to uninstall the free version and looking up the full version in the play store.
      • Having only one app makes quite sure the App is running fine on the customers device before he buys it. If customers are forced to switch apps, there is still a little risk that the full version will not run as good as the free one.

      But I can not tell, if the In-App approach provides better conversion results…

  1. I downloaded Glowing lines and I like it a lot! Really good light effects. I found the in app purchase there, but I didn’t find any in your meditation app.

    You don’t need to uninstall the free version and search for the full version on the appstore with a full and free version approach. It is possible to have a button with a link to the full version, so the user doesn’t need to search.

    The advantage with in app purchase instead of free/full version would be that only 2 clicks are needed to buy an app. 3 clicks are required with free/full version.

    Can you tell me how high your conversation rate is? My company Mobile Visuals have 25 live wallpapers on Google play(for instance Morphing tunnels). The conversation rates are usually 1 to 2%. I am considering to try in app purchase in one live wallpaper, but I am not sure if it’s worth the effort.

    • Stefan says:

      The Meditation App is free – there are no ads or any payments in it. It was my very first app and I didnt want to have the pressure to provide support for a paid app without experience.

      About 33% of the people that tried my Live Wallpeper kept it installed, and nearly 10% of them bought the full version – so I think the conversion rate is actually quite good.
      I would definitely give it a try – and remember: You don’t have to limit yourself just to free/full version. You can even unlock several features by in-app payment for a very
      low charge each – for example „feature1: 50ct, feature2: 50ct, special offer: both features now for 95ct“

      • Stefan says:

        I did not ran into this problem yet – or nobody out there having this problem with my App did send me a bug report…
        Seems to be a tricky problem. Maybe you can benefit from recent bugfixes in this area when you increase the minimum API level and compile against it? I am using API level 11 as a minimum in my Tron Wallpaper App.

        • I tried with
          but it was the same error message. Does it make any difference if I increase the Java compiler level? It is 1.6 now. My project build target is Android 4.3, which seems to be the highest target.

          I don’t need to consume any purchases in the app, I only need it for „static response testing“ to remove the purchases so I can test again. Maybe I can test in another way, so I don’t have to consume any purchase?

    • Stefan says:

      I created a small example App that shows how I started when implementing a buy/consume step – check it out, maybe it helps you. It is working great on my Galaxy S2 device without any exceptions or problems… see the download link at the bottom of this blog post.

      • Thanks a lot! I got static response testing to work now when I used your example project.

        The developer guide says that test purchase should be tested as the next step. I try to get this to work, but I always get the message „this version of the application is not configured for billing through Google play“ even though I follow all the recommendations. It seems very difficult to get this type of testing to work, like this blog explains:

        http://blog.suda.pl/2013/04/the-hell-of-testing-google-play-in-app-billing/#comment-146

        Maybe it is enough to test with static responses or do you think that is necessary to test with test purchases before releasing the new version of the app?

  2. I uploaded my first app with in app billing yesterday. I got orders from the customers, so I can see that the in app billing works. I only tested with static responses, „test purchases“ was of no use and impossible to get to work.

    I added a link to your blog from my company’s site. I think your guide is very useful, because Google’s information on in app billing is very confusing and difficult to understand. I wouldn’t have got this to work without your blog!

  3. ThemeBowl says:

    Hi,Thanks for the tutorial.i would like to know how to add 2 SKU ? Say i use unmanaged content like buying coins for game and $1.99 for 100coins and $5.00 for 500 coins.

  4. Jfcc says:

    hello, excellent work, I entered the checkForPremium method (). But if isPremium is false, I want to launch an activity, but I get this error:
    08-15 16:38:14.228: W/ContextImpl(2184):
    Implicit intents with startService
    are not safe: Intent { act=keyguardclock.action.startservice }
    android.content.ContextWrapper.startService:506
    android.content.ContextWrapper.startService:506
    com.sec.android.app.keyguard.KeyguardClockWidgetProvider
    .onReceive:104

    and a part of your code for check isPremium:
    boolean isPremium = inv.hasPurchase(SKU);

    // Set the shared preferences to premium=true if the user owns it
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(Crea_fatture.this);
    prefs.edit()
    .putBoolean(KEY_PREMIUM_VERSION, isPremium)
    .apply();

    buyHelper.dispose();

    // Forward to the currect activity depending on premium / demo mode
    if (isPremium) {
    return;

    } else {
    Intent intent = new Intent(Crea_app.this,InAppBillingActivity.class);
    startActivity(intent);
    finish();
    // TODO start demo activity
    }

  5. dit says:

    Hi.
    Man könnte also prüfen, ob das Gerät gerootet ist oder nicht, falls nicht dann könnte man doch die SharedPreferences nutzen. Zumindest wird der Start für nicht gerootete Geräte schneller.

    Was meinst Du?

    Grüße

Es können keine Kommentare mehr abgegeben werden.