In-app billing was a major source of frustration for me. All I wanted was to enable purchases on my Moustachify app, but no. Instead they provided a semi-working project which involved lots of guess-work and poking around with code to see what was essential for my project. For this tutorial, I'll be working on my other app Diablo 2 Runewords.
In terms of a development, the documentation by Google is abysmal. The official documentation has pages and pages of text, much of which isn't of great help when it comes to implementation.
As with the helper class (latest revision is 5 at time of writing) IabHelper. They've provided sample code which is broken for devices which don't have Google Play Services. These uncaught crashes caused me to waste many hours in finding and implementing the most appropriate fix.
I have supplied my patched up code which fixes the "Play is on everything" assumption made in Google's helper class.
Note: I'll say upfront now that this tutorial covers managed/consumable product purchases and consumption of products. However, it does not cover subscription based billing but I don't think it'd be much different.
Note #2: Please excuse the length of this post. It took a long over a week to write up as it's nearly Christmas, meaning lots of time with family/friends and late nights. State of mind isn't exactly the best for proof reading :)
Preparing the online side of things
First of all, add this permission to your app.
<uses-permission android:name="com.android.vending.BILLING" />
Bump the "versionCode" up by one and compile a new signed APK. Upload this to your app's APK settings under "Alpha". As long as Google knows about this APK's billing permissions, it can be saved as draft or published to alpha - doesn't really matter.
Note: While testing, you should only use this versionCode. Bumping it up during dev will give you this error "Application Error : This version of the application is not configured for Market Billing. Check the help center for more information."
The reason why we do this first is because Google takes it's sweet time time propagating the changes (about 30mins to 4hrs), so it's best to get it out of the way early on.
However, immediately after uploading you should be able to create new products.
If you haven't done this, you wouldn't be able to access the "In-app Products" tab.
Preparing the test account
An interesting point to note is that you cannot test product purchasing using your development account. This means if you're logged into your phone using your developer account, then you have to sign out and create a new account in order to test your code. Damn you Google!
In your developer dashboard, go to global Settings (not the app specific settings).
Under account details, look for "License Testing" where you can add email addresses for users who are testing your apps. Any users logged into an Android device with the given emails will not be charged for testing your billing code, although they have to enter in valid credit card details.
For "License Test Response", make sure it's responding in a way you expect.
Creating products
Once in the "In-app Products" tab, start adding products.
Product type:
This determines the way it's associated with your account.
Once the product is created, you can't change it!
- Managed type can only be purchased once and is permanently associated with the user's account.
- Unmanaged type is a legacy artefact from the days of API v2. In v3, it behaves just like a managed type. You have to explicitly consume a managed product before it is removed from the account.
- Subscription type sets up automatic recurring billing (not covered by this post!)
For more information, click on the type you're interested in and read the descriptions.
Product ID:
This is the "product SKU" or model number. As with product type, once the product is created you can't change it.
Each product ID must:
- be unique (per app)
- compose of either lowercase letters (a-z), numbers (0-9), underline (_) and dot (.)
- start with lowercase alphabets or numbers
The naming convention is completely up to you, but try to name it by type. This helps you keep track of the products. For example:
- app_name.permanent.ad_removal
- app_name.permanent.gun_type_a
- app_name.permanent.gun_type_b
- app_name.consumable.refill_bullets
- app_name.consumable.extra_3_lives
Title, description, price:
The rest of it is fairly straight forward. It's completely up to you what to fill in here.
Reminder! Click save once you're done setting the price, and remember to activate it!
Setting up the API library
Make sure you've got the "Google Play Billing Library" via the "Android SDK Manager". At time of writing, this is at revision 5 (and it's buggy)
Getting the files
Once you've downloaded it, look for your Android SDK folder. It's shown at the top of the SDK manager (top left, where it says "SDK Path").
- In there, you'll find a folder called "SDK_PATH\extras\google\play_billing".
- Ignore "market_billing_r02.zip", that's for the old API v2 way of handling in-app billing.
- Look for a file called "IInAppBillingService.aidl"
- Copy "IInAppBillingService.aidl" into your project under the path "PROJECT\src\com\android\vending\billing" (and yes, it has to be exactly the same!)
- Now go to "SDK_PATH\extras\google\play_billing\samples\TrivialDrive\src\com\example\android\trivialdrivesample\util" (yeah I'm not kidding, it's ridiculously long)
- Copy all 9 java source files file into your project under "PROJECT\src\common\services\billing\". You don't have to put it there, but I thought it was appropriate.
Making sure the files are patched
Google really let the ball drop with this library. Their new billing system is awesome, but they didn't even bother to release a stable library API files to accompany it.
I hate this bit because it took a few good hours out of my development time just to go research and tweak shit to get this up and running. It's a shame that I even have to write this part up.
What I've fixed is the NullPointerException and service binding errors when there is no Play Store installed on the device. What it should do is notify the user, not crash randomly or raise NPE's. And just for completion sake, serializable class IabException is missing a serialVersionUID.
There are other fixes posted online, but I feel there's too much error checking in too many different places. A sure way to cultivate a great source of bugs. Personally I like to find problems early and stop them before the service tries to do anything else.
Without further adieu, here's the diff for IabHelper.java. If you're looking for a quick download with the patches included, grab the fixed IabHelper.java from my github repo (or the diff).
--- ~/Android/sdk/extras/google/play_billing/samples/TrivialDrive/src/com/example/android/trivialdrivesample/util/IabHelper.java Fri Nov 29 18:55:07 2013
+++ ~/Android/project/src/twig/nguyen/common/services/billing/IabHelper.java Fri Dec 20 12:24:09 2013
@@ -22,6 +22,7 @@
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.content.ServiceConnection;
+import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -80,6 +81,8 @@
// Has this object been disposed of? (If so, we should ignore callbacks, etc)
boolean mDisposed = false;
+ boolean mIsBound = false;
+
// Are subscriptions supported?
boolean mSubscriptionsSupported = false;
@@ -264,9 +267,11 @@
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
serviceIntent.setPackage("com.android.vending");
- if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
+ List ri = mContext.getPackageManager().queryIntentServices(serviceIntent, 0);
+
+ if (ri != null && !ri.isEmpty()) {
// service available to handle that Intent
- mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+ mIsBound = mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
}
else {
// no service available to handle that Intent
@@ -289,7 +294,9 @@
mSetupDone = false;
if (mServiceConn != null) {
logDebug("Unbinding from service.");
- if (mContext != null) mContext.unbindService(mServiceConn);
+ if (mContext != null && mIsBound) {
+ mContext.unbindService(mServiceConn);
+ }
}
mDisposed = true;
mContext = null;
Querying user purchases from account
You'll have to learn to assume is that the user has already purchased a product and this is the first time they're running the app on a new phone. Either that or they're logged into multiple devices, purchased a product from you on a tablet and then use your app on a phone.
Query the billing service as soon as you can. I put mine into the onCreate() of the main activity. This makes it effortless for the user to switch between devices and keep their billing information in sync. Good news is that subsequent queries are cached locally by the billing service, so it's very quick.
Although you can put the following code in an Activity, I suggest putting it in a fragment in case of rotation config changes resetting your query.
Once the query finishes, we save the result to a variable within the app to simplify our checks.
You can download BillingInventoryFragment.java from github.
import twig.nguyen.common.services.billing.IabHelper;
import twig.nguyen.common.services.billing.IabResult;
import twig.nguyen.common.services.billing.Inventory;
import twig.nguyen.common.services.billing.Purchase;
import android.os.Bundle;
import android.util.Log;
import com.actionbarsherlock.app.SherlockFragment;
/**
* Helper fragment helps keep the billing madness out of MainActivity.
*
* @author twig
*/
public class BillingInventoryFragment extends SherlockFragment {
// Helper billing object
private IabHelper mHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
initialiseBilling();
}
private void initialiseBilling() {
if (mHelper != null) {
return;
}
// Create the helper, passing it our context and the public key to verify signatures with
mHelper = new IabHelper(getActivity(), G.getApplicationKey());
// Enable debug logging (for a production application, you should set this to false).
// mHelper.enableDebugLogging(true);
// Start setup. This is asynchronous and the specified listener will be called once setup completes.
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
@Override
public void onIabSetupFinished(IabResult result) {
// Have we been disposed of in the meantime? If so, quit.
if (mHelper == null) {
return;
}
// Something went wrong
if (!result.isSuccess()) {
Log.e(getActivity().getApplicationInfo().name, "Problem setting up in-app billing: " + result.getMessage());
return;
}
// IAB is fully set up. Now, let's get an inventory of stuff we own.
mHelper.queryInventoryAsync(iabInventoryListener());
}
});
}
/**
* Listener that's called when we finish querying the items and subscriptions we own
*/
private IabHelper.QueryInventoryFinishedListener iabInventoryListener() {
return new IabHelper.QueryInventoryFinishedListener() {
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
// Have we been disposed of in the meantime? If so, quit.
if (mHelper == null) {
return;
}
// Something went wrong
if (!result.isSuccess()) {
return;
}
// Do your checks here...
// Do we have the premium upgrade?
Purchase purchasePro = inventory.getPurchase(G.SKU_PRO); // Where G.SKU_PRO is your product ID (eg. permanent.ad_removal)
G.settings.isPro = (purchasePro != null && G.verifyDeveloperPayload(purchasePro));
// After checking inventory, re-jig stuff which the user can access now
// that we've determined what they've purchased
G.initialiseStuff();
}
};
}
/**
* Very important!
*/
@Override
public void onDestroy() {
super.onDestroy();
if (mHelper != null) {
mHelper.dispose();
mHelper = null;
}
}
}
G is a class I use to store global variables or statics for quick and dirty access.
- G.getApplicationKey() obfuscates the application key from prying eyes. You can find this from "All applications" > your app > "Services & APIs" > "Your license key for this application". The reason why this is a function (and not a final static string) is that we're meant to generate this during runtime to guard against basic decompilation. How you wish to implement this is up to you.
- Another point of interest is G.verifyDeveloperPayload(). The sample code in TrivialDriveSample wasn't entirely helpful; it simply returns true. Somehow this has to be generated from the user's information, but at the same time must be verified on multiple devices (so don't use IMEI).
I'm not sure how much this matters to you, but it's possible to verify the payload check by implementing some custom server code to keep track of a "receipt" on purchase and then use that receipt to verify on inventory check. Again, how you wish to implement this is entirely up to you.
Now that you're able to determine what inventory your user has, you can update the UI accordingly (ticks where item is purchased, disable buy now button, etc)
Purchasing products
The code below is written in an Activity, but it could just as easily be ported over to a fragment as all the magic pretty much happens in the OnIabPurchaseFinishedListener handler.
Building upon the stuff learned from querying the inventory, the code used to purchase products is fairly similar. You'll need to initialise the billing service helper and then make a purchase request, coupled with a handler.
This is a fairly chunky piece of code, so feel free to grab UpgradeActivity.java from github for easier viewing.
import twig.nguyen.common.services.billing.IabHelper;
import twig.nguyen.common.services.billing.IabResult;
import twig.nguyen.common.services.billing.Purchase;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Window;
public class UpgradeActivity extends SherlockFragmentActivity {
// Billing helper object
private IabHelper mHelper;
private boolean mBillingServiceReady;
@Override
protected void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_upgrade);
// Initialise buy buttons
Button btn = (Button) findViewById(R.id.btnUpgrade);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onButtonUpgradeClicked();
}
});
updateInventoryUI();
initialiseBilling();
}
private void initialiseBilling() {
if (mHelper != null) {
return;
}
// Create the helper, passing it our context and the public key to verify signatures with
mHelper = new IabHelper(this, G.getApplicationKey());
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
@Override
public void onIabSetupFinished(IabResult result) {
// Have we been disposed of in the meantime? If so, quit.
if (mHelper == null) {
return;
}
if (!result.isSuccess()) {
// Oh noes, there was a problem.
complain("Problem setting up in-app billing: " + result.getMessage());
return;
}
// IAB is fully set up.
mBillingServiceReady = true;
// Custom function to update UI reflecting their inventory
updateInventoryUI();
}
});
}
// User clicked the "Upgrade to Premium" button.
public void onButtonUpgradeClicked() {
if (!mBillingServiceReady) {
Toast.makeText(UpgradeActivity.this, "Purchase requires Google Play Store (billing) on your Android.", Toast.LENGTH_LONG).show();
return;
}
String payload = generatePayloadForSKU(G.SKU_PRO); // This is based off your own implementation.
mHelper.launchPurchaseFlow(UpgradeActivity.this, G.SKU_PRO, G.BILLING_REQUEST_CODE, mPurchaseFinishedListener, payload);
}
/**
* When In-App billing is done, it'll return information via onActivityResult().
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mHelper == null) {
return;
}
// Pass on the activity result to the helper for handling
if (!mHelper.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);
}
}
/**
* Very important
*/
@Override
public void onDestroy() {
super.onDestroy();
if (mHelper != null) {
mHelper.dispose();
mHelper = null;
}
}
// Callback for when a purchase is finished
private IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
@Override
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
// if we were disposed of in the meantime, quit.
if (mHelper == null) {
return;
}
// Don't complain if cancelling
if (result.getResponse() == IabHelper.IABHELPER_USER_CANCELLED) {
return;
}
if (!result.isSuccess()) {
complain("Error purchasing: " + result.getMessage());
return;
}
if (!G.verifyDeveloperPayload(purchase)) {
complain("Error purchasing. Authenticity verification failed.");
return;
}
// Purchase was success! Update accordingly
if (purchase.getSku().equals(G.SKU_PRO)) {
Toast.makeText(UpgradeActivity.this, "Thank you for upgrading!", Toast.LENGTH_LONG).show();
G.settings.isPro = true;
G.initialiseStuff();
// Update the UI to reflect their latest purchase
updateInventoryUI();
}
// Consume product immediately
else if (purchase.getSku().equals(G.SKU_CONSUMABLE_BULLETS)) {
// Snippet shown below ...
}
}
};
}
There are only a few things happening here, but it's been bloated by comments and inline objects. Some notes to keep in mind:
- generatePayloadForSKU() is not necessary as it's used for verifyDeveloperPayload()
- complain() is just a placeholder for your own "message to user about errors" function.
- BILLING_REQUEST_CODE is just a request code, similar to for "request" in startActivityForResult().
- There is no event trigger for SKU_CONSUMABLE_BULLETS as the handler is purely there for example.
The summary below explains what's happening in the code. Reading both from top-to-bottom explains the following logic flow:
- onCreate(): Initialise the billing service and a buy button
- initialiseBilling(): The billing service detects if the service is available, updates mBillingServiceReady and the UI accordingly. Optionally, you can delay the UI update until after an extra query for the latest inventory is done to ensure the most up to date inventory is used.
- onButtonUpgradeClicked(): Hander for the upgrade button simply launches the in-app billing process.
- onActivityResult(): This gives IabHelper.handleActivityResult() a chance to do it's thing.
- onIabPurchaseFinished(): This is where the magic happens.More details below.
- onDestroy(): Always important to do some house keeping.
Until now, there isn't really anything special. mPurchaseFinishedListener is where the brains of this exercise lies.
Breaking down onIabPurchaseFinished(), we'll see that:
- We need a special case for detecting IabHelper.IABHELPER_USER_CANCELLED. This causes result.isSuccess() to be false, but we don't wanna display an error message if the user simply cancels it.
- Subsequently, if the result really DID fail for reasons other than cancelling then we should notify the user.
- Upgrading to PRO means that it's a permanent purchase. Because of that we need to do something about it locally. Straight forward stuff.
Consuming purchases
Just below that PRO purchase example is another use-case where we've just purchased a consumable (G.SKU_CONSUMABLE_BULLETS). Consumables are purchases we don't want to leave as "purchased" against a user account (like extra lives or bullets). We should consume those immediately after purchase where possible.
mHelper.consumeAsync(purchase, handler) makes it really easy to do just that.
mHelper.consumeAsync(purchase, new IabHelper.OnConsumeFinishedListener() {
@Override
public void onConsumeFinished(Purchase purchase, IabResult result) {
// if we were disposed of in the meantime, quit.
if (mHelper == null) {
return;
}
if (result.isSuccess()) {
Toast.makeText(UpgradeActivity.this, "Bullet pack purchased", Toast.LENGTH_LONG).show();
// Example of what you need to do
playerBullets += 100;
}
else {
complain("Error while consuming: " + result);
}
// Update the UI to reflect their latest purchase
updateInventoryUI();
}
});
- As always, check if mHelper is still valid.
- And check for success state, apply consumable appropriately.
- Otherwise complain loudly about what went wrong.
- Update the UI accordingly.
Protecting the code
You should always use some sort of protection against prying eyes. Although there is no sure 100% way of keeping your app safe, you can always take simple precautions to prevent people from getting their hands on sensitive information such as your secret app keys.
One effective way is to use ProGuard on your app at compile time for signed apps. I won't be covering that in this post.
As mentioned before, you should also obfuscate your application key where possible.
Sources
In no particular order (or perhaps in the order which I ran into issues with?)
- In-App Billing Version 3 | Android Developers Blog
- Android Simple InApp Billing / Payment V3 | Blundell
- Integrating Google Play In-app Billing into an Android Application – A Tutorial - Techotopia
- Testing Your In-app Billing Application | Android Developers
- Testing In-app Billing | Android Developers
- Security and Design | Android Developers