[TUT] Simple InApp Billing / Payment (V2)

*DEPRECATED Please see my new tutorial: Simple InApp Billing / Payment V3*

________________________

—> InAppPurchaseTUT Source Project Download <--- Hi guys, To start with I'd like to say, this isn't the only way to do this. I myself have followed the developer tutorials on developer.android and managed to create this test project which I can now use as a reference when making other projects. Feel free to comment on the style and ask any questions. I would recommend you download the test project I have attached rather than copy and paste, this will stop minor syntax errors. Developer.Android Links: Google Billing Overview
Google Billing Integration

Going to try and walk you through creating an inapp payment method with the new android market payment system. This isn’t live yet (you can’t publish the app for general use) but you can upload it to the market and do full testing.

Ok first the outline.

Were going to create and app that has one button, when you click this button it informs the android market you want to make a purchase. On confirmation of this purchase you have bought a picture of a passport and it is shown to you in the app.

First things first.

Create a new project.

This project has a Main Activity XML and Class:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   >
        <TextView  
                android:paddingTop="20dip"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="Would you like to purchase this item?"
            />
        <Button
                android:id="@+id/main_purchase_yes"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="YES!!"
                />
        <ImageView
                android:id="@+id/main_purchase_item"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:src="@drawable/item"
                android:visibility="gone"
                />
</LinearLayout>
package com.blundell.test;
 
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
 
public class AppMainTest extends Activity implements OnClickListener{
   
        private static final String TAG = "BillingService";
       
        private Context mContext;
        private ImageView purchaseableItem;
        private Button purchaseButton;
       
        /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i("BillingService", "Starting");
        setContentView(R.layout.main);
         
        mContext = this;
       
        purchaseButton = (Button) findViewById(R.id.main_purchase_yes);
        purchaseButton.setOnClickListener(this);
        purchaseableItem = (ImageView) findViewById(R.id.main_purchase_item);
       
    }
 
        @Override
        public void onClick(View v) {
                switch (v.getId()) {
                case R.id.main_purchase_yes:
                       
                        break;
                default:
                        // nada
                        Log.i(TAG,"default. ID: "+v.getId());
                        break;
                }
               
        }
       
        private void showItem() {
                purchaseableItem.setVisibility(View.VISIBLE);
        }
 
        @Override
        protected void onPause() {
                BillingHelper.stopService();
                super.onPause();
        }
}

You could now run this project up and it won’t do anything. Fine.

There is an AIDL file that you have to include your project, this gives you access to the Remote Methods of the Android Market service. Don’t worry about this, just know that this is what is under the com.android.vending.billing package in the test project.

InApp Billing in android basically means:
Tell the Android Market what the user is buying In the test project it is “android.test.purchase”
The Android Market compares this against a list you create (on your publisher page) [url=Testing]https://developer.android.com/guide/market/billing/billing_testing.html[/url]
User fills in their details You dont have to worry about this
You receive a message saying purchase complete or failed.

In order to tell the Android Market what you are buying you need to be running the billing service. ( A service that talks between your app and the android market) This looks like:

package com.blundell.test;
 
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
 
import com.android.vending.billing.IMarketBillingService;
 
public class BillingService extends Service implements ServiceConnection{
       
        private static final String TAG = "BillingService";
       
        /** The service connection to the remote MarketBillingService. */
        private IMarketBillingService mService;
       
        @Override
        public void onCreate() {
                super.onCreate();
                Log.i(TAG, "Service starting with onCreate");
               
                try {
                        boolean bindResult = bindService(new Intent("com.android.vending.billing.MarketBillingService.BIND"), this, Context.BIND_AUTO_CREATE);
                        if(bindResult){
                                Log.i(TAG,"Market Billing Service Successfully Bound");
                        } else {
                                Log.e(TAG,"Market Billing Service could not be bound.");
                                //TODO stop user continuing
                        }
                } catch (SecurityException e){
                        Log.e(TAG,"Market Billing Service could not be bound. SecurityException: "+e);
                        //TODO stop user continuing
                }
        }
       
        public void setContext(Context context) {
        attachBaseContext(context);
    }
       
        @Override
        public IBinder onBind(Intent intent) {
                return null;
        }
 
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
                Log.i(TAG, "Market Billing Service Connected.");
                mService = IMarketBillingService.Stub.asInterface(service);
                BillingHelper.instantiateHelper(getBaseContext(), mService);
        }
 
        @Override
        public void onServiceDisconnected(ComponentName name) {
               
        }
 
}

Now I have wrapped this BillingService in a BillingHelper, the helper will do all your work for you (call the market with purchases):

package com.blundell.test;
 
import java.util.ArrayList;
 
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
 
import com.android.vending.billing.IMarketBillingService;
import com.blundell.test.BillingSecurity.VerifiedPurchase;
import com.blundell.test.C.ResponseCode;
 
public class BillingHelper {
 
        private static final String TAG = "BillingService";
       
        private static IMarketBillingService mService;
        private static Context mContext;
        private static Handler mCompletedHandler;
       
        protected static VerifiedPurchase latestPurchase;
       
        protected static void instantiateHelper(Context context, IMarketBillingService service) {
                mService = service;
                mContext = context;
        }
 
        protected static void setCompletedHandler(Handler handler){
                mCompletedHandler = handler;
        }
       
        protected static boolean isBillingSupported() {
                if (amIDead()) {
                        return false;
                }
                Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED");
                if (mService != null) {
                        try {
                                Bundle response = mService.sendBillingRequest(request);
                                ResponseCode code = ResponseCode.valueOf((Integer) response.get("RESPONSE_CODE"));
                                Log.i(TAG, "isBillingSupported response was: " + code.toString());
                                if (ResponseCode.RESULT_OK.equals(code)) {
                                        return true;
                                } else {
                                        return false;
                                }
                        } catch (RemoteException e) {
                                Log.e(TAG, "isBillingSupported response was: RemoteException", e);
                                return false;
                        }
                } else {
                        Log.i(TAG, "isBillingSupported response was: BillingService.mService = null");
                        return false;
                }
        }
       
        /**
         * A REQUEST_PURCHASE request also triggers two asynchronous responses (broadcast intents).
         * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides error information about the request. (which I ignore)
         * Next, if the request was successful, the Android Market application sends an IN_APP_NOTIFY broadcast intent.
         * This message contains a notification ID, which you can use to retrieve the transaction details for the REQUEST_PURCHASE
         * @param activityContext
         * @param itemId
         */
        protected static void requestPurchase(Context activityContext, String itemId){
                if (amIDead()) {
                        return;
                }
                Log.i(TAG, "requestPurchase()");
                Bundle request = makeRequestBundle("REQUEST_PURCHASE");
                request.putString("ITEM_ID", itemId);
                try {
                        Bundle response = mService.sendBillingRequest(request);
                       
                        //The RESPONSE_CODE key provides you with the status of the request
                        Integer responseCodeIndex       = (Integer) response.get("RESPONSE_CODE");
                        //The PURCHASE_INTENT key provides you with a PendingIntent, which you can use to launch the checkout UI
                        PendingIntent pendingIntent = (PendingIntent) response.get("PURCHASE_INTENT");
                        //The REQUEST_ID key provides you with a unique request identifier for the request
                        Long requestIndentifier         = (Long) response.get("REQUEST_ID");
                        Log.i(TAG, "current request is:" + requestIndentifier);
                        C.ResponseCode responseCode = C.ResponseCode.valueOf(responseCodeIndex);
                        Log.i(TAG, "REQUEST_PURCHASE Sync Response code: "+responseCode.toString());
                       
                        startBuyPageActivity(pendingIntent, new Intent(), activityContext);
                } catch (RemoteException e) {
                        Log.e(TAG, "Failed, internet error maybe", e);
                        Log.e(TAG, "Billing supported: "+isBillingSupported());
                }
        }
       
        /**
         * A GET_PURCHASE_INFORMATION request also triggers two asynchronous responses (broadcast intents).
         * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request.  (which I ignore)
         * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent.
         * This message contains detailed transaction information.
         * The transaction information is contained in a signed JSON string (unencrypted).
         * The message includes the signature so you can verify the integrity of the signed string
         * @param notifyIds
         */
        protected static void getPurchaseInformation(String[] notifyIds){
                if (amIDead()) {
                        return;
                }
                Log.i(TAG, "getPurchaseInformation()");
                Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION");
                // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate.
                // The Android Market application returns this nonce with the PURCHASE_STATE_CHANGED broadcast intent so you can verify the integrity of the transaction information.
                request.putLong("NONCE", BillingSecurity.generateNonce());
                // The NOTIFY_IDS key contains an array of notification IDs, which you received in the IN_APP_NOTIFY broadcast intent.
                request.putStringArray("NOTIFY_IDS", notifyIds);
                try {
                        Bundle response = mService.sendBillingRequest(request);
                       
                        //The REQUEST_ID key provides you with a unique request identifier for the request
                        Long requestIndentifier         = (Long) response.get("REQUEST_ID");
                        Log.i(TAG, "current request is:" + requestIndentifier);
                        //The RESPONSE_CODE key provides you with the status of the request
                        Integer responseCodeIndex       = (Integer) response.get("RESPONSE_CODE");
                        C.ResponseCode responseCode = C.ResponseCode.valueOf(responseCodeIndex);
                        Log.i(TAG, "GET_PURCHASE_INFORMATION Sync Response code: "+responseCode.toString());
                       
                } catch (RemoteException e) {
                        Log.e(TAG, "Failed, internet error maybe", e);
                        Log.e(TAG, "Billing supported: "+isBillingSupported());
                }
        }
 
        /**
         * To acknowledge that you received transaction information you send a
         * CONFIRM_NOTIFICATIONS request.
         *
         * A CONFIRM_NOTIFICATIONS request triggers a single asynchronous response—a RESPONSE_CODE broadcast intent.
         * This broadcast intent provides status and error information about the request.
         *
         * Note: As a best practice, you should not send a CONFIRM_NOTIFICATIONS request for a purchased item until you have delivered the item to the user.
         * This way, if your application crashes or something else prevents your application from delivering the product,
         * your application will still receive an IN_APP_NOTIFY broadcast intent from Android Market indicating that you need to deliver the product
         * @param notifyIds
         */
        protected static void confirmTransaction(String[] notifyIds) {
                if (amIDead()) {
                        return;
                }
                Log.i(TAG, "confirmTransaction()");
                Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS");
                request.putStringArray("NOTIFY_IDS", notifyIds);
                try {
                        Bundle response = mService.sendBillingRequest(request);
 
                        //The REQUEST_ID key provides you with a unique request identifier for the request
                        Long requestIndentifier         = (Long) response.get("REQUEST_ID");
                        Log.i(TAG, "current request is:" + requestIndentifier);
                       
                        //The RESPONSE_CODE key provides you with the status of the request
                        Integer responseCodeIndex       = (Integer) response.get("RESPONSE_CODE");
                        C.ResponseCode responseCode = C.ResponseCode.valueOf(responseCodeIndex);
                       
                        Log.i(TAG, "CONFIRM_NOTIFICATIONS Sync Response code: "+responseCode.toString());
                } catch (RemoteException e) {
                        Log.e(TAG, "Failed, internet error maybe", e);
                        Log.e(TAG, "Billing supported: " + isBillingSupported());
                }
        }
       
        /**
         *
         * Can be used for when a user has reinstalled the app to give back prior purchases.
         * if an item for sale's purchase type is "managed per user account" this means google will have a record ofthis transaction
         *
         * A RESTORE_TRANSACTIONS request also triggers two asynchronous responses (broadcast intents).
         * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request.
         * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent.
         * This message contains the detailed transaction information. The transaction information is contained in a signed JSON string (unencrypted).
         * The message includes the signature so you can verify the integrity of the signed string
         * @param nonce
         */
        protected static void restoreTransactionInformation(Long nonce) {
                if (amIDead()) {
                        return;
                }
                Log.i(TAG, "confirmTransaction()");
                Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS");
                // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate
                request.putLong("NONCE", nonce);
                try {
                        Bundle response = mService.sendBillingRequest(request);
 
                        //The REQUEST_ID key provides you with a unique request identifier for the request
                        Long requestIndentifier         = (Long) response.get("REQUEST_ID");
                        Log.i(TAG, "current request is:" + requestIndentifier);
                       
                        //The RESPONSE_CODE key provides you with the status of the request
                        Integer responseCodeIndex       = (Integer) response.get("RESPONSE_CODE");
                        C.ResponseCode responseCode = C.ResponseCode.valueOf(responseCodeIndex);
                        Log.i(TAG, "RESTORE_TRANSACTIONS Sync Response code: "+responseCode.toString());
                } catch (RemoteException e) {
                        Log.e(TAG, "Failed, internet error maybe", e);
                        Log.e(TAG, "Billing supported: " + isBillingSupported());
                }
        }
       
        private static boolean amIDead() {
                if (mService == null || mContext == null) {
                        Log.e(TAG, "BillingHelper not fully instantiated");
                        return true;
                } else {
                        return false;
                }
        }
 
        private static Bundle makeRequestBundle(String method) {
                Bundle request = new Bundle();
                request.putString("BILLING_REQUEST", method);
                request.putInt("API_VERSION", 1);
                request.putString("PACKAGE_NAME", mContext.getPackageName());
                return request;
        }
       
        /**
         *
         *
         * You must launch the pending intent from an activity context and not an application context
         * You cannot use the singleTop launch mode to launch the pending intent
         * @param pendingIntent
         * @param intent
         * @param context
         */
        private static void startBuyPageActivity(PendingIntent pendingIntent, Intent intent, Context context){
                //TODO add above 2.0 implementation with reflection, for now just using 1.6 implem
               
                // This is on Android 1.6. The in-app checkout page activity will be on its
            // own separate activity stack instead of on the activity stack of
            // the application.
                try {
                        pendingIntent.send(context, 0, intent);                
                } catch (CanceledException e){
                        Log.e(TAG, "startBuyPageActivity CanceledException");
                }
        }
 
         protected static void verifyPurchase(String signedData, String signature) {
                ArrayList<VerifiedPurchase> purchases = BillingSecurity.verifyPurchase(signedData, signature);
                if(purchases != null && !purchases.isEmpty()){
                        latestPurchase = purchases.get(0);
                       
                        confirmTransaction(new String[]{latestPurchase.notificationId});
                } else {
                       Log.d(TAG, "BillingHelper.verifyPurchase error. purchases was null");
                }
               
                if(mCompletedHandler != null){
                        mCompletedHandler.sendEmptyMessage(0);
                } else {
                        Log.e(TAG, "verifyPurchase error. Handler not instantiated. Have you called setCompletedHandler()?");
                }
        }
       
        public static void stopService(){
                mContext.stopService(new Intent(mContext, BillingService.class));
                mService = null;
                mContext = null;
                mCompletedHandler = null;
                Log.i(TAG, "Stopping Service");
        }
}

When a request to the android market from your BillingHelper is instantiated, the market sends back certain intent’s these need to be caught in a receiver and dealt with appropriately (so the market will say “yes I confirm this user has just bought…” ):

package com.blundell.test;
 
import static com.blundell.test.C.ACTION_NOTIFY;
import static com.blundell.test.C.ACTION_PURCHASE_STATE_CHANGED;
import static com.blundell.test.C.ACTION_RESPONSE_CODE;
import static com.blundell.test.C.INAPP_REQUEST_ID;
import static com.blundell.test.C.INAPP_RESPONSE_CODE;
import static com.blundell.test.C.INAPP_SIGNATURE;
import static com.blundell.test.C.INAPP_SIGNED_DATA;
import static com.blundell.test.C.NOTIFICATION_ID;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
 
public class BillingReceiver extends BroadcastReceiver {
 
        private static final String TAG = "BillingService";
 
        @Override
        public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                Log.i(TAG, "Received action: " + action);
        if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
            String signedData = intent.getStringExtra(INAPP_SIGNED_DATA);
            String signature = intent.getStringExtra(INAPP_SIGNATURE);
            purchaseStateChanged(context, signedData, signature);
        } else if (ACTION_NOTIFY.equals(action)) {
            String notifyId = intent.getStringExtra(NOTIFICATION_ID);
            notify(context, notifyId);
        } else if (ACTION_RESPONSE_CODE.equals(action)) {
            long requestId = intent.getLongExtra(INAPP_REQUEST_ID, -1);
            int responseCodeIndex = intent.getIntExtra(INAPP_RESPONSE_CODE, C.ResponseCode.RESULT_ERROR.ordinal());
            checkResponseCode(context, requestId, responseCodeIndex);
        } else {
           Log.e(TAG, "unexpected action: " + action);
        }
        }
 
 
        private void purchaseStateChanged(Context context, String signedData, String signature) {
                Log.i(TAG, "purchaseStateChanged got signedData: " + signedData);
                Log.i(TAG, "purchaseStateChanged got signature: " + signature);
                BillingHelper.verifyPurchase(signedData, signature);
        }
       
        private void notify(Context context, String notifyId) {
                Log.i(TAG, "notify got id: " + notifyId);
                String[] notifyIds = {notifyId};
                BillingHelper.getPurchaseInformation(notifyIds);
        }
       
        private void checkResponseCode(Context context, long requestId, int responseCodeIndex) {
                Log.i(TAG, "checkResponseCode got requestId: " + requestId);
                Log.i(TAG, "checkResponseCode got responseCode: " + C.ResponseCode.valueOf(responseCodeIndex));
        }
}

To keep the code a bit tidier all constants are stored in a class called C:

package com.blundell.test;
 
 
public class C {
 
        // The response codes for a request, defined by Android Market.
        public enum ResponseCode {
                RESULT_OK,
                RESULT_USER_CANCELED,
                RESULT_SERVICE_UNAVAILABLE,
                RESULT_BILLING_UNAVAILABLE,
                RESULT_ITEM_UNAVAILABLE,
                RESULT_DEVELOPER_ERROR,
                RESULT_ERROR;
 
                // Converts from an ordinal value to the ResponseCode
                public static ResponseCode valueOf(int index) {
                        ResponseCode[] values = ResponseCode.values();
                        if (index < 0 || index >= values.length) {
                                return RESULT_ERROR;
                        }
                        return values[index];
                }
        }
 
        // The possible states of an in-app purchase, as defined by Android Market.
    public enum PurchaseState {
        // Responses to requestPurchase or restoreTransactions.
        PURCHASED,   // User was charged for the order.
        CANCELED,    // The charge failed on the server.
        REFUNDED;    // User received a refund for the order.
 
        // Converts from an ordinal value to the PurchaseState
        public static PurchaseState valueOf(int index) {
            PurchaseState[] values = PurchaseState.values();
            if (index < 0 || index >= values.length) {
                return CANCELED;
            }
            return values[index];
        }
    }
       
        // These are the names of the extras that are passed in an intent from
    // Market to this application and cannot be changed.
    public static final String NOTIFICATION_ID = "notification_id";
    public static final String INAPP_SIGNED_DATA = "inapp_signed_data";
    public static final String INAPP_SIGNATURE = "inapp_signature";
    public static final String INAPP_REQUEST_ID = "request_id";
    public static final String INAPP_RESPONSE_CODE = "response_code";
       
        // Intent actions that we send from the BillingReceiver to the
        // BillingService. Defined by this application.
        public static final String ACTION_CONFIRM_NOTIFICATION = "com.example.dungeons.CONFIRM_NOTIFICATION";
        public static final String ACTION_GET_PURCHASE_INFORMATION = "com.example.dungeons.GET_PURCHASE_INFORMATION";
        public static final String ACTION_RESTORE_TRANSACTIONS = "com.example.dungeons.RESTORE_TRANSACTIONS";
 
        // Intent actions that we receive in the BillingReceiver from Market.
        // These are defined by Market and cannot be changed.
        public static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY";
        public static final String ACTION_RESPONSE_CODE = "com.android.vending.billing.RESPONSE_CODE";
        public static final String ACTION_PURCHASE_STATE_CHANGED = "com.android.vending.billing.PURCHASE_STATE_CHANGED";
       
}

And finally, this code I took from the Android sample project. What it does is decrypt the messages that the Android Market is sending back to you and package them into a purchased item object. YOU NEED TO EDIT ONE LINE, to add your public key from your market account (edit profile):

// Copyright 2010 Google Inc. All Rights Reserved.
 
package com.blundell.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.blundell.test.C.PurchaseState;
import com.blundell.test.util.Base64;
import com.blundell.test.util.Base64DecoderException;
 
/**
 * Security-related methods. For a secure implementation, all of this code
 * should be implemented on a server that communicates with the application on
 * the device. For the sake of simplicity and clarity of this example, this code
 * is included here and is executed on the device. If you must verify the
 * purchases on the phone, you should obfuscate this code to make it harder for
 * an attacker to replace the code with stubs that treat all purchases as
 * verified.
 */
public class BillingSecurity {
        private static final String TAG = "BillingService";
 
        private static final String KEY_FACTORY_ALGORITHM = "RSA";
        private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
        private static final SecureRandom RANDOM = new SecureRandom();
 
        /**
         * This keeps track of the nonces that we generated and sent to the server.
         * We need to keep track of these until we get back the purchase state and
         * send a confirmation message back to Android Market. If we are killed and
         * lose this list of nonces, it is not fatal. Android Market will send us a
         * new "notify" message and we will re-generate a new nonce. This has to be
         * "static" so that the {@link BillingReceiver} can check if a nonce exists.
         */
        private static HashSet<Long> sKnownNonces = new HashSet<Long>();
 
        /**
         * A class to hold the verified purchase information.
         */
        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 boolean isPurchased(){
                        return purchaseState.equals(PurchaseState.PURCHASED);
                }
               
               
        }
 
        /** Generates a nonce (a random number used once). */
        public static long generateNonce() {
                long nonce = RANDOM.nextLong();
                Log.i(TAG, "Nonce generateD: "+nonce);
                sKnownNonces.add(nonce);
                return nonce;
        }
 
        public static void removeNonce(long nonce) {
                sKnownNonces.remove(nonce);
        }
 
        public static boolean isNonceKnown(long nonce) {
                return sKnownNonces.contains(nonce);
        }
 
        /**
         * Verifies that the data was signed with the given signature, and returns
         * the list of verified purchases. The data is in JSON format and contains a
         * nonce (number used once) that we generated and that was signed (as part
         * of the whole data string) with a private key. The data also contains the
         * {@link PurchaseState} and product ID of the purchase. In the general
         * case, there can be an array of purchase transactions because there may be
         * delays in processing the purchase on the backend and then several
         * purchases can be batched together.
         *
         * @param signedData
         *            the signed JSON string (signed, not encrypted)
         * @param signature
         *            the signature for the data, signed with the private key
         */
        public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData, String signature) {
                if (signedData == null) {
                        Log.e(TAG, "data is null");
                        return null;
                }
                Log.i(TAG, "signedData: " + signedData);
                boolean verified = false;
                if (!TextUtils.isEmpty(signature)) {
                        /**
                         * Compute your public key (that you got from the Android Market
                         * publisher site).
                         *
                         * Instead of just storing the entire literal string here embedded
                         * in the program, construct the key at runtime from pieces or use
                         * bit manipulation (for example, XOR with some other string) to
                         * hide the actual key. The key itself is not secret information,
                         * but we don't want to make it easy for an adversary to replace the
                         * public key with one of their own and then fake messages from the
                         * server.
                         *
                         * Generally, encryption keys / passwords should only be kept in
                         * memory long enough to perform the operation they need to perform.
                         */
                        String base64EncodedPublicKey = "PUT YOUR PUBLIC KEY HERE";
                        PublicKey key = BillingSecurity.generatePublicKey(base64EncodedPublicKey);
                        verified = BillingSecurity.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);
 
                        // The nonce might be null if the user backed out of the buy page.
                        nonce = jObject.optLong("nonce");
                        jTransactionsArray = jObject.optJSONArray("orders");
                        if (jTransactionsArray != null) {
                                numTransactions = jTransactionsArray.length();
                        }
                } catch (JSONException e) {
                        return null;
                }
 
                if (!BillingSecurity.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");
                                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 the purchase state is PURCHASED, then we require a
                                // verified nonce.
                                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;
        }
 
        /**
         * Generates a PublicKey instance from a string containing the
         * Base64-encoded public key.
         *
         * @param encodedPublicKey
         *            Base64-encoded public key
         * @throws IllegalArgumentException
         *             if encodedPublicKey is invalid
         */
        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, "Base64DecoderException.", e);
                        return null;
                }
        }
 
        /**
         * Verifies that the signature from the server matches the computed
         * signature on the data. Returns true if the data is correctly signed.
         *
         * @param publicKey
         *            public key associated with the developer account
         * @param signedData
         *            signed data from server
         * @param signature
         *            server signature
         * @return true if the data and signature match
         */
        public static boolean verify(PublicKey publicKey, String signedData, String signature) {
                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, "Base64DecoderException.", e);
                }
                return false;
        }
}

That is it. Now to get this to work you have to add to your activity class.

In your onCreate you need to start the market service.

startService(new Intent(mContext, BillingService.class));

In the onClick is where we are going to request to make a purchase:

@Override
        public void onClick(View v) {
                switch (v.getId()) {
                case R.id.main_purchase_yes:
                        if(BillingHelper.isBillingSupported()){
                                BillingHelper.requestPurchase(mContext, "android.test.purchased");
                                // android.test.purchased or android.test.canceled or android.test.refunded
                } else {
                        Log.i(TAG,"Can't purchase on this device");
                }
                       
                        break;
                default:
                        // nada
                        Log.i(TAG,"default. ID: "+v.getId());
                        break;
                }
               
        }

Now the BillingService won’t do anything on it’s own, so you need to load up the BillingHelper. The billing helper uses a handler to send callbacks on completed purchases:

BillingHelper.setCompletedHandler(mTransactionHandler);

This handler needs to be declared in your activity:

 public Handler mTransactionHandler = new Handler(){
                public void handleMessage(android.os.Message msg) {
                        Log.i(TAG, "Transaction complete");
                        Log.i(TAG, "Transaction status: "+BillingHelper.latestPurchase.purchaseState);
                        Log.i(TAG, "Item attempted purchase is: "+BillingHelper.latestPurchase.productId);
                       
                        if(BillingHelper.latestPurchase.isPurchased()){
                                showItem();
                        } else {
                               // Failure
                        }
                };     
    };

Once you have all this don’t forget to update your manifest! You need to tell it your using a service, a broadcast reciever and those intents you wish to receive (from the android market):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
     package="com.blundell.test"
     android:versionCode="2"
     android:versionName="1.0">
    <uses-sdk android:minSdkVersion="4" />
 
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".AppMainTest"
                 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=".BillingService" />
               
                <receiver android:name=".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-permission android:name="com.android.vending.BILLING" />
   
</manifest>
 

This code tutorial isn’t foolproof and I feel I may of swept over a few things. But I really want to just give an alternative to the tutorial that is on the developer.android site. You could read this and understand the smaller concepts then go on to make a better implementation.

Caveats:
May have issues with multiple purchases and network delays
I am not responsible if you use this code in a production envrionment.
Testing with real applications https://developer.android.com/guide/market/billing/billing_testing.html
Please obfuscate your code to ensure people can’t get your purchases for free!
https://developer.android.com/guide/market/billing/billing_best_practices.html

—> InAppPurchaseTUT Source Project Download <--- Remember to say thanks and enjoy! ________________________ *DEPRECATED Please see my new tutorial: Simple InApp Billing / Payment V3*


171 thoughts on “[TUT] Simple InApp Billing / Payment (V2)

  1. Hey i have follow your tutorial but when my payment is successful the application force stop instead of show the passport picture.
    here is my log is:
    08-22 15:03:26.560: E/AndroidRuntime(25716): FATAL EXCEPTION: main
    08-22 15:03:26.560: E/AndroidRuntime(25716): Process: com.blundell.test, PID: 25716
    08-22 15:03:26.560: E/AndroidRuntime(25716): java.lang.RuntimeException: Unable to start receiver com.blundell.test.BillingReceiver: java.lang.NullPointerException
    08-22 15:03:26.560: E/AndroidRuntime(25716): at android.app.ActivityThread.handleReceiver(ActivityThread.java:2567)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at android.app.ActivityThread.access$1800(ActivityThread.java:161)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1341)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at android.os.Handler.dispatchMessage(Handler.java:102)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at android.os.Looper.loop(Looper.java:157)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at android.app.ActivityThread.main(ActivityThread.java:5356)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at java.lang.reflect.Method.invokeNative(Native Method)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at java.lang.reflect.Method.invoke(Method.java:515)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1265)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1081)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at dalvik.system.NativeStart.main(Native Method)
    08-22 15:03:26.560: E/AndroidRuntime(25716): Caused by: java.lang.NullPointerException
    08-22 15:03:26.560: E/AndroidRuntime(25716): at com.blundell.test.BillingHelper.verifyPurchase(BillingHelper.java:249)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at com.blundell.test.BillingReceiver.purchaseStateChanged(BillingReceiver.java:44)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at com.blundell.test.BillingReceiver.onReceive(BillingReceiver.java:27)
    08-22 15:03:26.560: E/AndroidRuntime(25716): at android.app.ActivityThread.handleReceiver(ActivityThread.java:2552)

Comments are closed.