Android In-App Integration Tutorial
Today I am going post about In-App Billing.
In-app products are the digital goods that you offer for sale from inside your application to users. Examples of digital goods includes in-game currency, application feature upgrades that enhance the user experience, and new content for your application.
Below i explained step by step. How to Integrate In-App Billing in Your Application.
*Note.
Don't Test In-App In emulator or unsigned application.
AndroidManifest.xml file you need to give permission
<uses-permission android:name="com.android.vending.BILLING"
/>
AndroidManifest.xml
<?xml version="1.0"
encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vj.test"
android:versionCode="9"
android:versionName="1.9" >
<uses-permission android:name="com.android.vending.BILLING"
/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
/>
<application
android:icon="@drawable/icon"
android:label="@string/app_name" >
<activity
android:name="com.vj.test.Mainactivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN"
/>
<category android:name="android.intent.category.LAUNCHER"
/>
</intent-filter>
</activity>
<service android:name="com.vj.test.BillingService"
/>
<receiver android:name="com.vj.test.BillingReceiver"
>
<intent-filter>
<action android:name="com.android.vending.billing.IN_APP_NOTIFY"
/>
<action android:name="com.android.vending.billing.RESPONSE_CODE"
/>
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED"
/>
</intent-filter>
</receiver>
</application>
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="16" />
</manifest>
Then come to Activity class
here you need to add your in-app product ID
private static final CatalogEntry[] CATALOG = new CatalogEntry[] {
new CatalogEntry("Your Product
ID", R.string.abc123, Managed.MANAGED),
new CatalogEntry("Your Product
ID ", R.string.abc1234, Managed.MANAGED),
new CatalogEntry("Your Product
ID ", R.string.ab123200, Managed.MANAGED)
};
Full Source Code Activity Class
package com.vj.test;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import
android.app.Activity;
import
android.app.AlertDialog;
import android.app.Dialog;
import
android.content.Context;
import
android.content.DialogInterface;
import
android.content.Intent;
import
android.content.SharedPreferences;
import
android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.Html;
import
android.text.SpannableStringBuilder;
import android.util.Log;
import android.view.View;
import
android.view.View.OnClickListener;
import
android.view.ViewGroup;
import
android.widget.AdapterView;
import
android.widget.AdapterView.OnItemSelectedListener;
import
android.widget.ArrayAdapter;
import
android.widget.Button;
import
android.widget.SimpleCursorAdapter;
import
android.widget.Spinner;
import
android.widget.Toast;
import
com.vj.test.BillingService.RequestPurchase;
import
com.vj.test.BillingService.RestoreTransactions;
import
com.vj.test.Consts.PurchaseState;
import
com.vj.test.Consts.ResponseCode;
public class Mainactivity extends Activity implements OnClickListener,
OnItemSelectedListener
{
private static final String TAG = "Dungeons";
private static final String DB_INITIALIZED
= "db_initialized";
private DungeonsPurchaseObserver mDungeonsPurchaseObserver;
private Handler mHandler;
private BillingService mBillingService;
private Button mBuyButton;
private Spinner mSelectItemSpinner;
private PurchaseDatabase mPurchaseDatabase;
private Cursor mOwnedItemsCursor;
private Set<String> mOwnedItems = new HashSet<String>();
private String mPayloadContents = null;
private static final int DIALOG_CANNOT_CONNECT_ID
= 1;
private static final int DIALOG_BILLING_NOT_SUPPORTED_ID
= 2;
private static final int DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID
= 3;
private enum Managed {
MANAGED,
UNMANAGED, SUBSCRIPTION
}
private class DungeonsPurchaseObserver extends PurchaseObserver
{
public DungeonsPurchaseObserver(Handler handler)
{
super(Mainactivity.this, handler);
}
@Override
public void onBillingSupported(boolean supported, String type) {
if (Consts.DEBUG)
{
Log.i(TAG,
"supported:
"
+ supported);
}
if ((type
== null) || type.equals(Consts.ITEM_TYPE_INAPP))
{
if (supported)
{
restoreDatabase();
mBuyButton.setEnabled(true);
}
else {
showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
}
}
else if (type.equals(Consts.ITEM_TYPE_SUBSCRIPTION))
{
mCatalogAdapter.setSubscriptionsSupported(supported);
}
else {
showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
}
}
@Override
public void onPurchaseStateChange(PurchaseState purchaseState,
String itemId, int quantity, long purchaseTime,
String developerPayload)
{
if (Consts.DEBUG)
{
Log.i(TAG,
"onPurchaseStateChange()
itemId: " + itemId + " "
+
purchaseState);
}
if (developerPayload
== null) {
logProductActivity(itemId,
purchaseState.toString());
}
else {
logProductActivity(itemId,
purchaseState + "\n\t"
+
developerPayload);
}
if (purchaseState
== PurchaseState.PURCHASED)
{
mOwnedItems.add(itemId);
for (CatalogEntry e : CATALOG)
{
if (e.sku.equals(itemId)
&&
e.managed.equals(Managed.SUBSCRIPTION))
{
}
}
}
mCatalogAdapter.setOwnedItems(mOwnedItems);
mOwnedItemsCursor.requery();
}
@Override
public void onRequestPurchaseResponse(RequestPurchase request,
ResponseCode responseCode)
{
if (Consts.DEBUG)
{
Log.d(TAG,
request.mProductId + ": " + responseCode);
}
if (responseCode
== ResponseCode.RESULT_OK)
{
if (Consts.DEBUG)
{
Log.i(TAG,
"purchase
was successfully sent to server");
}
logProductActivity(request.mProductId,
"sending
purchase request");
}
else if (responseCode
== ResponseCode.RESULT_USER_CANCELED)
{
if (Consts.DEBUG)
{
Log.i(TAG,
"user
canceled purchase");
}
logProductActivity(request.mProductId,
"dismissed
purchase dialog");
}
else {
if (Consts.DEBUG)
{
Log.i(TAG,
"purchase
failed");
}
logProductActivity(request.mProductId,
"request
purchase returned " + responseCode);
}
}
@Override
public void onRestoreTransactionsResponse(RestoreTransactions request,
ResponseCode responseCode)
{
if (responseCode
== ResponseCode.RESULT_OK)
{
if (Consts.DEBUG)
{
Log.d(TAG,
"completed
RestoreTransactions request");
}
SharedPreferences
prefs = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor
edit = prefs.edit();
edit.putBoolean(DB_INITIALIZED,
true);
edit.commit();
}
else {
if (Consts.DEBUG)
{
Log.d(TAG,
"RestoreTransactions
error: " + responseCode);
}
}
}
}
private static class CatalogEntry {
public String sku;
public int nameId;
public Managed managed;
public CatalogEntry(String sku, int nameId, Managed managed)
{
this.sku = sku;
this.nameId = nameId;
this.managed = managed;
}
}
private static final CatalogEntry[] CATALOG = new CatalogEntry[] {
new CatalogEntry("your product id", R.string.abc123, Managed.MANAGED),
new CatalogEntry("your product id", R.string.abc1234, Managed.MANAGED),
new CatalogEntry("your product id", R.string.ab123200, Managed.MANAGED)
};
private String mItemName;
private String mSku;
private Managed mManagedType;
private CatalogAdapter mCatalogAdapter;
@SuppressWarnings("deprecation")
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mHandler = new Handler();
mDungeonsPurchaseObserver = new DungeonsPurchaseObserver(mHandler);
mBillingService = new BillingService();
mBillingService.setContext(this);
mPurchaseDatabase = new PurchaseDatabase(this);
setupWidgets();
ResponseHandler.register(mDungeonsPurchaseObserver);
if (!mBillingService.checkBillingSupported())
{
showDialog(DIALOG_CANNOT_CONNECT_ID);
}
if (!mBillingService
.checkBillingSupported(Consts.ITEM_TYPE_SUBSCRIPTION))
{
showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
}
}
@Override
protected void onStart()
{
super.onStart();
ResponseHandler.register(mDungeonsPurchaseObserver);
initializeOwnedItems();
}
@Override
protected void onStop()
{
super.onStop();
ResponseHandler.unregister(mDungeonsPurchaseObserver);
}
@Override
protected void onDestroy()
{
super.onDestroy();
mPurchaseDatabase.close();
mBillingService.unbind();
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState)
{
super.onRestoreInstanceState(savedInstanceState);
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id)
{
case DIALOG_CANNOT_CONNECT_ID:
return createDialog(R.string.cannot_connect_title,
R.string.cannot_connect_message);
case DIALOG_BILLING_NOT_SUPPORTED_ID:
return createDialog(R.string.billing_not_supported_title,
R.string.billing_not_supported_message);
case DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID:
return createDialog(R.string.subscriptions_not_supported_title,
R.string.subscriptions_not_supported_message);
default:
return null;
}
}
private Dialog createDialog(int titleId, int messageId)
{
String helpUrl = replaceLanguageAndRegion(getString(R.string.help_url));
if (Consts.DEBUG)
{
Log.i(TAG,
helpUrl);
}
final Uri
helpUri = Uri.parse(helpUrl);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(titleId)
.setIcon(android.R.drawable.stat_sys_warning)
.setMessage(messageId)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.learn_more,
new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface
dialog,
int which)
{
Intent intent = new Intent(Intent.ACTION_VIEW,
helpUri);
startActivity(intent);
}
});
return builder.create();
}
private String replaceLanguageAndRegion(String str) {
if (str.contains("%lang%") || str.contains("%region%")) {
Locale locale = Locale.getDefault();
str
= str.replace("%lang%", locale.getLanguage().toLowerCase());
str
= str.replace("%region%", locale.getCountry().toLowerCase());
}
return str;
}
private void setupWidgets()
{
mBuyButton = (Button) findViewById(R.id.buy_button);
mBuyButton.setEnabled(false);
mBuyButton.setOnClickListener(this);
mSelectItemSpinner = (Spinner) findViewById(R.id.item_choices);
mCatalogAdapter = new CatalogAdapter(this, CATALOG);
mSelectItemSpinner.setAdapter(mCatalogAdapter);
mSelectItemSpinner.setOnItemSelectedListener(this);
mOwnedItemsCursor = mPurchaseDatabase.queryAllPurchasedItems();
startManagingCursor(mOwnedItemsCursor);
String[] from = new String[] {
PurchaseDatabase.PURCHASED_PRODUCT_ID_COL,
PurchaseDatabase.PURCHASED_QUANTITY_COL
};
int[] to = new int[] { R.id.item_name, R.id.item_quantity
};
new SimpleCursorAdapter(this, R.layout.item_row, mOwnedItemsCursor,
from,
to);
}
private void prependLogEntry(CharSequence
cs) {
SpannableStringBuilder contents = new SpannableStringBuilder(cs);
contents.append('\n');
}
private void logProductActivity(String product, String activity)
{
SpannableStringBuilder contents = new SpannableStringBuilder();
contents.append(Html.fromHtml("<b>" + product + "</b>:
"));
contents.append(activity);
prependLogEntry(contents);
}
private void restoreDatabase()
{
SharedPreferences
prefs = getPreferences(MODE_PRIVATE);
boolean initialized =
prefs.getBoolean(DB_INITIALIZED, false);
if (!initialized)
{
mBillingService.restoreTransactions();
Toast.makeText(this, R.string.restoring_transactions,
Toast.LENGTH_LONG).show();
}
}
private void initializeOwnedItems()
{
new Thread(new Runnable() {
@Override
public void run()
{
doInitializeOwnedItems();
}
}).start();
}
private void doInitializeOwnedItems()
{
Cursor
cursor = mPurchaseDatabase.queryAllPurchasedItems();
if (cursor == null) {
return;
}
final Set<String> ownedItems = new HashSet<String>();
try {
int productIdCol =
cursor
.getColumnIndexOrThrow(PurchaseDatabase.PURCHASED_PRODUCT_ID_COL);
while (cursor.moveToNext())
{
String productId =
cursor.getString(productIdCol);
ownedItems.add(productId);
}
}
finally {
cursor.close();
}
mHandler.post(new Runnable() {
@Override
public void run()
{
mOwnedItems.addAll(ownedItems);
mCatalogAdapter.setOwnedItems(mOwnedItems);
}
});
}
@Override
public void onClick(View v) {
if (v == mBuyButton) {
if (Consts.DEBUG)
{
Log.d(TAG,
"buying:
"
+ mItemName + " sku: " + mSku);
}
if ((mManagedType != Managed.SUBSCRIPTION)
&&
!mBillingService.requestPurchase(mSku,
Consts.ITEM_TYPE_INAPP,
mPayloadContents)) {
showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
}
else if (!mBillingService.requestPurchase(mSku,
Consts.ITEM_TYPE_SUBSCRIPTION,
mPayloadContents)) {
showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
}
}
}
@Override
public void onItemSelected(AdapterView<?>
parent, View view, int position,
long id) {
mItemName = getString(CATALOG[position].nameId);
mSku = CATALOG[position].sku;
mManagedType = CATALOG[position].managed;
}
@Override
public void onNothingSelected(AdapterView<?>
arg0) {
}
private static class CatalogAdapter extends ArrayAdapter<String> {
private CatalogEntry[] mCatalog;
private Set<String> mOwnedItems = new HashSet<String>();
private boolean mIsSubscriptionsSupported = false;
public CatalogAdapter(Context
context, CatalogEntry[] catalog) {
super(context, android.R.layout.simple_spinner_item);
mCatalog = catalog;
for (CatalogEntry element : catalog)
{
add(context.getString(element.nameId));
}
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
public void setOwnedItems(Set<String> ownedItems)
{
mOwnedItems = ownedItems;
notifyDataSetChanged();
}
public void setSubscriptionsSupported(boolean supported)
{
mIsSubscriptionsSupported = supported;
}
@Override
public boolean areAllItemsEnabled()
{
return false;
}
@Override
public boolean isEnabled(int position)
{
CatalogEntry entry = mCatalog[position];
if ((entry.managed == Managed.MANAGED)
&&
mOwnedItems.contains(entry.sku)) {
return false;
}
if ((entry.managed == Managed.SUBSCRIPTION)
&&
!mIsSubscriptionsSupported) {
return false;
}
return true;
}
@Override
public View getDropDownView(int position, View convertView,
ViewGroup
parent) {
View view = super.getDropDownView(position,
convertView, parent);
view.setEnabled(isEnabled(position));
return view;
}
}
}
Then Add In-App product
Go to Developer account -> Add Apk Signed Apk -> Then you can able to see In-App product Navigation then click -> Add New Product - >Manage Product -> Enter Product ID and Mandatory field -> Then Make it Active Product.
Check the below Screen Shot
*This product ID only you have to add in above the code for every product.
Then you need base64EncodedPublicKey
This key you need to add Security.Java class line no 81
Cehck this screen shot
Copy this key paste it.
Security.Java class full source code
package com.vj.test;
import
java.security.InvalidKeyException;
import
java.security.KeyFactory;
import
java.security.NoSuchAlgorithmException;
import
java.security.PublicKey;
import
java.security.SecureRandom;
import
java.security.Signature;
import
java.security.SignatureException;
import
java.security.spec.InvalidKeySpecException;
import
java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashSet;
import org.json.JSONArray;
import
org.json.JSONException;
import org.json.JSONObject;
import
android.text.TextUtils;
import android.util.Log;
import
com.example.dungeons.util.Base64;
import
com.example.dungeons.util.Base64DecoderException;
import
com.vj.test.Consts.PurchaseState;
public class Security {
private static final String TAG = "Security";
private static final String KEY_FACTORY_ALGORITHM
= "RSA";
private static final String SIGNATURE_ALGORITHM
= "SHA1withRSA";
private static final SecureRandom RANDOM = new SecureRandom();
private static HashSet<Long> sKnownNonces = new HashSet<Long>();
public static class VerifiedPurchase {
public PurchaseState purchaseState;
public String notificationId;
public String productId;
public String orderId;
public long purchaseTime;
public String developerPayload;
public VerifiedPurchase(PurchaseState purchaseState,
String notificationId,
String productId, String orderId,
long purchaseTime,
String developerPayload)
{
this.purchaseState = purchaseState;
this.notificationId = notificationId;
this.productId = productId;
this.orderId = orderId;
this.purchaseTime = purchaseTime;
this.developerPayload = developerPayload;
}
}
public static long generateNonce()
{
long nonce = RANDOM.nextLong();
sKnownNonces.add(nonce);
return nonce;
}
public static void removeNonce(long nonce)
{
sKnownNonces.remove(nonce);
}
public static boolean isNonceKnown(long nonce)
{
return sKnownNonces.contains(nonce);
}
public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData,
String signature)
{
if (signedData
== null) {
Log.e(TAG,
"data
is null");
return null;
}
if (Consts.DEBUG)
{
Log.i(TAG,
"signedData:
"
+ signedData);
}
boolean verified = false;
if (!TextUtils.isEmpty(signature))
{
String base64EncodedPublicKey = "Here Your Public KEey";
PublicKey
key = Security.generatePublicKey(base64EncodedPublicKey);
verified
= Security.verify(key, signedData,
signature);
if (!verified)
{
Log.w(TAG,
"signature
does not match data.");
return null;
}
}
JSONObject jObject;
JSONArray jTransactionsArray =
null;
int numTransactions = 0;
long nonce = 0L;
try {
jObject
= new JSONObject(signedData);
nonce
= jObject.optLong("nonce");
jTransactionsArray
= jObject.optJSONArray("orders");
if (jTransactionsArray
!= null) {
numTransactions
= jTransactionsArray.length();
}
}
catch (JSONException e) {
return null;
}
if (!Security.isNonceKnown(nonce))
{
Log.w(TAG,
"Nonce
not found: " + nonce);
return null;
}
ArrayList<VerifiedPurchase> purchases = new ArrayList<VerifiedPurchase>();
try {
for (int i = 0; i <
numTransactions; i++) {
JSONObject jElement =
jTransactionsArray.getJSONObject(i);
int response = jElement.getInt("purchaseState");
PurchaseState purchaseState = PurchaseState.valueOf(response);
String productId =
jElement.getString("productId");
@SuppressWarnings("unused")
String packageName =
jElement.getString("packageName");
long purchaseTime =
jElement.getLong("purchaseTime");
String orderId = jElement.optString("orderId", "");
String notifyId = null;
if (jElement.has("notificationId")) {
notifyId
= jElement.getString("notificationId");
}
String developerPayload =
jElement.optString(
"developerPayload", null);
if ((purchaseState
== PurchaseState.PURCHASED)
&& !verified) {
continue;
}
purchases.add(new VerifiedPurchase(purchaseState,
notifyId,
productId,
orderId, purchaseTime, developerPayload));
}
}
catch (JSONException e) {
Log.e(TAG,
"JSON
exception: ", e);
return null;
}
removeNonce(nonce);
return purchases;
}
public static PublicKey generatePublicKey(String encodedPublicKey)
{
try {
byte[] decodedKey = Base64.decode(encodedPublicKey);
KeyFactory keyFactory = KeyFactory
.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory
.generatePublic(new X509EncodedKeySpec(decodedKey));
}
catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
catch (InvalidKeySpecException e) {
Log.e(TAG,
"Invalid
key specification.");
throw new IllegalArgumentException(e);
}
catch (Base64DecoderException e) {
Log.e(TAG,
"Base64
decoding failed.");
throw new IllegalArgumentException(e);
}
}
public static boolean verify(PublicKey
publicKey, String signedData,
String signature)
{
if (Consts.DEBUG)
{
Log.i(TAG,
"signature:
"
+ signature);
}
Signature
sig;
try {
sig
= Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature)))
{
Log.e(TAG,
"Signature
verification failed.");
return false;
}
return true;
}
catch (NoSuchAlgorithmException e) {
Log.e(TAG,
"NoSuchAlgorithmException.");
}
catch (InvalidKeyException e) {
Log.e(TAG,
"Invalid
key specification.");
}
catch (SignatureException e) {
Log.e(TAG,
"Signature
exception.");
}
catch (Base64DecoderException e) {
Log.e(TAG,
"Base64
decoding failed.");
}
return false;
}
}
Then publish the application
Wait for several hours.
Then download application enjoy with In-App Billing