How to make In App Purchase in Android Using Google Play Billing Library

Share Post
  •  
  •  
  •  
  • 1
  •  
  •  
  •  
  •  
  •  
  •  
  •  
    1
    Share

Please Subscribe Youtube| Like Facebook | Follow Twitter

Video

NOTE:

If you need personal technical assistance for integrating Google Play Billing Library with latest version than you can book order with me via Fiver here.

Introduction

In this article we will learn how to integrate and make in app purchase of Single Non-consumable one-time products using google play billing library.

It is recommended to read current article page first for understanding basic concepts of Google Play Billing.

Tips

ArticleJavaKotlin
For Single Non-consumable one-time productlinklink
For Multiple Non-consumable one-time productlinklink
For Single Consumable one-time productlinklink
For Multiple Consumable one-time productlinklink
For Single In App Subscriptionslinklink
For Multiple In App Subscriptionslinklink

In-app product types

Google Play Billing can be used to sell the following types of in-app products:

1)  One-time products: An in-app product requiring a single, non-recurring charge to the user’s form of payment. Additional game levels, premium loot boxes, and media files are examples of one-time products. The Google Play Console refers to one-time products as managed products, and the Google Play Billing library calls them “INAPP”.

Google Play Billing supports the following types of one-time products:

  • Non-consumable one-time products are products that provide a permanent effect, such as a premium upgrade. To avoid users from repurchasing these products, you shouldn’t indicate them as being consumed.
  • Consumable one-time products are products that provide temporary benefits and can be repurchased, such as additional in-game currency or extra game lives. To make a consumable one-time product available for purchase again, you need to send a consumption request to Google Play.

2)  Subscriptions: An in-app product requiring a recurring charge to the user’s form of payment. Online service is example of subscription. The Google Play Billing Library calls these “SUBS”.

Our Example

In this post we will demonstrate how to purchase Non-consumable one-time products. For example purpose we will use “purchase” as Non-consumable one-time product which will allow user to purchase it one time only. You can use this according to your need such as to remove ads or upgrade premium status etc.

In our example we have a textview which shows purchase status and a purchase button which allow user to purchase item. When item is bought purchase status will be changed to “purchased” and purchase button will be removed and we store purchase value in user preference.

On every app start we will check purchase status of item from Google Play Store Cache and reflect necessary changes accordingly because if user already purchased item previously and re-installs the app or switch to another device or refunded the purchased item, therefore we will store updated purchase status value in user preference.

Google Play Store Cache works best on both offline as well as online. When there is no internet Cache will contain last updated purchase status of item. When internet become available Play Store Cache will be updated automatically with latest purchase status of item. Google Play Store Cache updated roughly every 24 hours or immediately on every purchase.

Requirements:

  1. Android Project/App to which In App Purchase Item to be added
  2. Google play console account

Note: In order to create in app product items in Google Console or perform real test of in app purchase item in your app, you first need to Integrate Google Play Library and add billing permission in your project and then upload that apk on Google play console and publish it any track (Internal test track (recommended), Closed track, Open track, Production track). Therefore we will perform these actions first.

Steps

Follow below steps

1 Integrate Google Play Library in your project

2 Create an application and fill Store listing in Google play console

3 Upload and publish your app

4 Create product item “purchase” One-Time Managed Product in Google play console

5 Code in app purchase flow logic.

6 Run and Test the application

1 Integrate Google Play Billing Library in your project.

First add Google Play Billing Library dependency at app level of your build.gradle file.

implementation 'com.android.billingclient:billing:3.0.1'

In manifest file add below permission

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

2 Create an application and fill Store listing in Google play console

First go to your Google play console and create an application

After creating App fill necessary Store listing

3 Upload and publish your app

After necessary Store listing is filled go to app release and open internal test track and click on create release.

Upload your apk, fill necessary info then review and publish/release your app in internal test track.

4 Create product item “purchase” One-Time Managed Product in Google play console

After releasing app in internal test go to Store presence -> In-app products -> Managed products

Click on create managed product

Enter product id “purchase”, title, description and click on active.

Add price details for demo purpose we have entered rs 500 PKR

Then click on save

After saving product item will be created

5 Code in app purchase flow logic.

First make sure you have added Google Play Billing Library dependency and billing permission, which was described in step 1.

Then Add textview Purchase Status and Purchase button in activity_main.xml Layout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.programtown.example.MainActivity">

    <TextView
        android:id="@+id/purchase_status"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"

          />

    <Button
        android:id="@+id/purchase_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Purchase"
        android:onClick="purchase"
    />
</LinearLayout>

In order to make In-App purchase our MainActivity needs to implements PurchasesUpdatedListener interface and override onPurchasesUpdated() method which will be called every time to get notifications for purchases updates. Both purchases initiated by your app and the ones initiated outside of your app will be reported here.

We also need to create BillingClient Object and set listener to it so that we can communicate with Google Play Billing Service.

public class MainActivity extends AppCompatActivity implements PurchasesUpdatedListener {

    private BillingClient billingClient;
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        billingClient = BillingClient.newBuilder(this)
                .enablePendingPurchases().setListener(this).build();
    }

    @Override
    public void onPurchasesUpdated(BillingResult billingResult, @Nullable 
 List<Purchase> purchases) {
        //we will code purchase result logic later
    }
}

On purchase button click we will initiate purchase flow

//initiate purchase on button click
public void purchase(View view) {
	//check if service is already connected
	if (billingClient.isReady()) {
		initiatePurchase();
	}
	//else reconnect service
	else{
		billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
		billingClient.startConnection(new BillingClientStateListener() {
			@Override
			public void onBillingSetupFinished(BillingResult billingResult) {
				if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
					initiatePurchase();
				} else {
					Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
				}
			}
			@Override
			public void onBillingServiceDisconnected() {
			}
		});
	}
}

In initiatePurchase() method we have included product id “purchase”, which was added in Google Play Console inside in app products, specified product type as INAPP. Then we have called querySkuDetailsAsync() method which will query our product from Play console. After querying we will call launchBillingFlow() method which will display purchase dialog. Result of purchase dialog will be reported inside onPurchasesUpdated() method which we will code next.

private void initiatePurchase() {
	List<String> skuList = new ArrayList<>();
	skuList.add(PRODUCT_ID);
	SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
	params.setSkusList(skuList).setType(INAPP);
	billingClient.querySkuDetailsAsync(params.build(),
			new SkuDetailsResponseListener() {
				@Override
				public void onSkuDetailsResponse(BillingResult billingResult,
												 List<SkuDetails> skuDetailsList) {
					if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
						if (skuDetailsList != null && skuDetailsList.size() > 0) {
							BillingFlowParams flowParams = BillingFlowParams.newBuilder()
									.setSkuDetails(skuDetailsList.get(0))
									.build();
							billingClient.launchBillingFlow(MainActivity.this, flowParams);
						}
						else{
							//try to add item/product id "purchase" inside managed product in google play console
							Toast.makeText(getApplicationContext(),"Purchase Item not Found",Toast.LENGTH_SHORT).show();
						}
					} else {
						Toast.makeText(getApplicationContext(),
								" Error "+billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
					}
				}
			});
}

In onPurchasesUpdated() method we will check purchase result and handle it accordingly.

@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
	//if item newly purchased
	if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
		handlePurchases(purchases);
	}
	//if item already purchased then check and reflect changes
	else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
		Purchase.PurchasesResult queryAlreadyPurchasesResult = billingClient.queryPurchases(INAPP);
		List<Purchase> alreadyPurchases = queryAlreadyPurchasesResult.getPurchasesList();
		if(alreadyPurchases!=null){
			handlePurchases(alreadyPurchases);
		}
	}
	//if purchase cancelled
	else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
		Toast.makeText(getApplicationContext(),"Purchase Canceled",Toast.LENGTH_SHORT).show();
	}
	// Handle any other error msgs
	else {
		Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
	}
}

In handlePurchases() method we will handle, verify and acknowledge purchase. After the purchase, acknowledgement is necessary because failure to properly acknowledge purchase will result in purchase being refunded. After the acknowledgement we will save purchase value in preference and restart activity to make necessary changes e.g. change purchase status, hide purchase button.

This method also toast user to complete transaction if purchase status is pending. In case of unspecified purchase state we will change purchase status to “Not Purchased” and store updated value in preference.

void handlePurchases(List<Purchase>  purchases) {
	for(Purchase purchase:purchases) {
		//if item is purchased
		if (PRODUCT_ID.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
		{
			if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
				// Invalid purchase
				// show error to user
				Toast.makeText(getApplicationContext(), "Error : Invalid Purchase", Toast.LENGTH_SHORT).show();
				return;
			}
			// else purchase is valid
			//if item is purchased and not acknowledged
			if (!purchase.isAcknowledged()) {
				AcknowledgePurchaseParams acknowledgePurchaseParams =
						AcknowledgePurchaseParams.newBuilder()
								.setPurchaseToken(purchase.getPurchaseToken())
								.build();
				billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase);
			}
			//else item is purchased and also acknowledged
			else {
				// Grant entitlement to the user on item purchase
				// restart activity
				if(!getPurchaseValueFromPref()){
					savePurchaseValueToPref(true);
					Toast.makeText(getApplicationContext(), "Item Purchased", Toast.LENGTH_SHORT).show();
					this.recreate();
				}
			}
		}
		//if purchase is pending
		else if( PRODUCT_ID.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
		{
			Toast.makeText(getApplicationContext(),
					"Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
		}
		//if purchase is unknown
		else if(PRODUCT_ID.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
		{
			savePurchaseValueToPref(false);
			purchaseStatus.setText("Purchase Status : Not Purchased");
			purchaseButton.setVisibility(View.VISIBLE);
			Toast.makeText(getApplicationContext(), "Purchase Status Unknown", Toast.LENGTH_SHORT).show();
		}
	}
}

AcknowledgePurchaseResponseListener ackPurchase = new AcknowledgePurchaseResponseListener() {
	@Override
	public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
		if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
			//if purchase is acknowledged
			// Grant entitlement to the user. and restart activity
			savePurchaseValueToPref(true);
			Toast.makeText(getApplicationContext(), "Item Purchased", Toast.LENGTH_SHORT).show();
			MainActivity.this.recreate();
		}
	}
};

For verification we have used client side signature verification, which will be performed with in app (less secure). However if you can afford server then you should do verification of purchase through your server (more secure).

In client side verification you need to add your developer’s public key of app.

For old Play Console Design To get key go to Developer Console > Select your app > Development Tools > Services & APIs.

For new Play Console Design To get key go to Developer Console > Select your app > Monetize > Monetization setup

Then paste that key to String variable base64key in below method.

/**
 * Verifies that the purchase was signed correctly for this developer's public key.
 * <p>Note: It's strongly recommended to perform such check on your backend since hackers can
 * replace this method with "constant true" if they decompile/rebuild your app.
 * </p>
 */
private boolean verifyValidSignature(String signedData, String signature) {
	try {
		// To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
		String base64Key = "Add Your Key Here"
		return Security.verifyPurchase(base64Key, signedData, signature);
	} catch (IOException e) {
		return false;
	}
}

Code of Security.java is at whole code section

Up to that point our purchase logic is complete. Now On every app start we will check purchase status of item from Google Play Store Cache using getPurchasesList() method and reflect necessary changes accordingly because if user already purchased item previously and reinstalls the app or switch to another device or refunded the purchased item therefore we should store updated purchase status in user preference. After querying we will call handlePurchases() method which will make necessary changes accordingly.

billingClient.startConnection(new BillingClientStateListener() {
    @Override
    public void onBillingSetupFinished(BillingResult billingResult) {
        if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
            Purchase.PurchasesResult queryPurchase = billingClient.queryPurchases(INAPP);
            List<Purchase> queryPurchases = queryPurchase.getPurchasesList();
            if(queryPurchases!=null && queryPurchases.size()>0){
                handlePurchases(queryPurchases);
            }
	    //if purchase list is empty that means item is not purchased
	    //Or purchase is refunded or canceled
	    else{
	        savePurchaseValueToPref(false);
	    }
        }
    }

    @Override
    public void onBillingServiceDisconnected() {
    }
});


//item Purchased
if(getPurchaseValueFromPref()){
    purchaseButton.setVisibility(View.GONE);
    purchaseStatus.setText("Purchase Status : Purchased");
}
//item not Purchased
else{
    purchaseButton.setVisibility(View.VISIBLE);
    purchaseStatus.setText("Purchase Status : Not Purchased");
}

Whole Code

App level build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    buildToolsVersion "29.0.3"
    defaultConfig {
        applicationId "com.programtown.example"
        minSdkVersion 17
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'

    implementation 'com.android.billingclient:billing:3.0.1'
}

Manifest File

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.programtown.example">

    <uses-permission android:name="com.android.vending.BILLING" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name="com.programtown.example.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.programtown.example.MainActivity">

    <TextView
        android:id="@+id/purchase_status"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"

          />

    <Button
        android:id="@+id/purchase_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Purchase"
        android:onClick="purchase"
    />

</LinearLayout>

MainActivity.java

package com.programtown.example;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import android.content.SharedPreferences;
import androidx.annotation.Nullable;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;


import static com.android.billingclient.api.BillingClient.SkuType.INAPP;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity implements PurchasesUpdatedListener {

    public static final String PREF_FILE= "MyPref";
    public static final String PURCHASE_KEY= "purchase";
    public static final String PRODUCT_ID= "purchase";

    TextView purchaseStatus;
    Button purchaseButton;

    private BillingClient billingClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        purchaseStatus=(TextView) findViewById(R.id.purchase_status);
        purchaseButton=(Button) findViewById(R.id.purchase_button);

        // Establish connection to billing client
        //check purchase status from google play store cache
        //to check if item already Purchased previously or refunded
        billingClient = BillingClient.newBuilder(this)
                .enablePendingPurchases().setListener(this).build();
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
                    Purchase.PurchasesResult queryPurchase = billingClient.queryPurchases(INAPP);
                    List<Purchase> queryPurchases = queryPurchase.getPurchasesList();
                    if(queryPurchases!=null && queryPurchases.size()>0){
                        handlePurchases(queryPurchases);
                    }
		    //if purchase list is empty that means item is not purchased
		    //Or purchase is refunded or canceled
		    else{
			savePurchaseValueToPref(false);
		    }
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
            }
        });

        //item Purchased
        if(getPurchaseValueFromPref()){
            purchaseButton.setVisibility(View.GONE);
            purchaseStatus.setText("Purchase Status : Purchased");
        }
        //item not Purchased
        else{
            purchaseButton.setVisibility(View.VISIBLE);
            purchaseStatus.setText("Purchase Status : Not Purchased");
        }
    }

    private SharedPreferences getPreferenceObject() {
        return getApplicationContext().getSharedPreferences(PREF_FILE, 0);
    }
    private SharedPreferences.Editor getPreferenceEditObject() {
        SharedPreferences pref = getApplicationContext().getSharedPreferences(PREF_FILE, 0);
        return pref.edit();
    }
    private boolean getPurchaseValueFromPref(){
        return getPreferenceObject().getBoolean( PURCHASE_KEY,false);
    }
    private void savePurchaseValueToPref(boolean value){
        getPreferenceEditObject().putBoolean(PURCHASE_KEY,value).commit();
    }

    //initiate purchase on button click
    public void purchase(View view) {
        //check if service is already connected
        if (billingClient.isReady()) {
            initiatePurchase();
        }
        //else reconnect service
        else{
            billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
            billingClient.startConnection(new BillingClientStateListener() {
                @Override
                public void onBillingSetupFinished(BillingResult billingResult) {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        initiatePurchase();
                    } else {
                        Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
                    }
                }
                @Override
                public void onBillingServiceDisconnected() {
                }
            });
        }
    }
    private void initiatePurchase() {
        List<String> skuList = new ArrayList<>();
        skuList.add(PRODUCT_ID);
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(skuList).setType(INAPP);
        billingClient.querySkuDetailsAsync(params.build(),
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(BillingResult billingResult,
                                                     List<SkuDetails> skuDetailsList) {
                        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                            if (skuDetailsList != null && skuDetailsList.size() > 0) {
                                BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                                        .setSkuDetails(skuDetailsList.get(0))
                                        .build();
                                billingClient.launchBillingFlow(MainActivity.this, flowParams);
                            }
                            else{
                                //try to add item/product id "purchase" inside managed product in google play console
                                Toast.makeText(getApplicationContext(),"Purchase Item not Found",Toast.LENGTH_SHORT).show();
                            }
                        } else {
                            Toast.makeText(getApplicationContext(),
                                    " Error "+billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }

    @Override
    public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
        //if item newly purchased
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            handlePurchases(purchases);
        }
        //if item already purchased then check and reflect changes
        else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
            Purchase.PurchasesResult queryAlreadyPurchasesResult = billingClient.queryPurchases(INAPP);
            List<Purchase> alreadyPurchases = queryAlreadyPurchasesResult.getPurchasesList();
            if(alreadyPurchases!=null){
                handlePurchases(alreadyPurchases);
            }
        }
        //if purchase cancelled
        else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
            Toast.makeText(getApplicationContext(),"Purchase Canceled",Toast.LENGTH_SHORT).show();
        }
        // Handle any other error msgs
        else {
            Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
        }
    }
    void handlePurchases(List<Purchase>  purchases) {
        for(Purchase purchase:purchases) {
            //if item is purchased
            if (PRODUCT_ID.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
            {
                if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
                    // Invalid purchase
                    // show error to user
                    Toast.makeText(getApplicationContext(), "Error : Invalid Purchase", Toast.LENGTH_SHORT).show();
                    return;
                }
                // else purchase is valid
                //if item is purchased and not acknowledged
                if (!purchase.isAcknowledged()) {
                    AcknowledgePurchaseParams acknowledgePurchaseParams =
                            AcknowledgePurchaseParams.newBuilder()
                                    .setPurchaseToken(purchase.getPurchaseToken())
                                    .build();
                    billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase);
                }
                //else item is purchased and also acknowledged
                else {
                    // Grant entitlement to the user on item purchase
                    // restart activity
                    if(!getPurchaseValueFromPref()){
                        savePurchaseValueToPref(true);
                        Toast.makeText(getApplicationContext(), "Item Purchased", Toast.LENGTH_SHORT).show();
                        this.recreate();
                    }
                }
            }
            //if purchase is pending
            else if( PRODUCT_ID.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
            {
                Toast.makeText(getApplicationContext(),
                        "Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
            }
            //if purchase is unknown
            else if(PRODUCT_ID.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
            {
                savePurchaseValueToPref(false);
                purchaseStatus.setText("Purchase Status : Not Purchased");
                purchaseButton.setVisibility(View.VISIBLE);
                Toast.makeText(getApplicationContext(), "Purchase Status Unknown", Toast.LENGTH_SHORT).show();
            }
        }
    }
    AcknowledgePurchaseResponseListener ackPurchase = new AcknowledgePurchaseResponseListener() {
        @Override
        public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
            if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
                //if purchase is acknowledged
                // Grant entitlement to the user. and restart activity
                savePurchaseValueToPref(true);
                Toast.makeText(getApplicationContext(), "Item Purchased", Toast.LENGTH_SHORT).show();
                MainActivity.this.recreate();
            }
        }
    };

    /**
     * Verifies that the purchase was signed correctly for this developer's public key.
     * <p>Note: It's strongly recommended to perform such check on your backend since hackers can
     * replace this method with "constant true" if they decompile/rebuild your app.
     * </p>
     */
    private boolean verifyValidSignature(String signedData, String signature) {
        try {
            // To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
            String base64Key = "Add Your Key Here";
            return Security.verifyPurchase(base64Key, signedData, signature);
        } catch (IOException e) {
            return false;
        }
    }
	
	@Override
    protected void onDestroy() {
        super.onDestroy();
        if(billingClient!=null){
            billingClient.endConnection();
        }
    }
}

Security.java

/*
 * Copyright (c) 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.programtown.example;

import android.text.TextUtils;
import android.util.Base64;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;

/**
 * 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.
 */
public class Security {
    private static final String TAG = "IABUtil/Security";

    private static final String KEY_FACTORY_ALGORITHM = "RSA";
    private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";

    /**
     * Verifies that the data was signed with the given signature, and returns the verified
     * purchase.
     * @param base64PublicKey the base64-encoded public key to use for verifying.
     * @param signedData the signed JSON string (signed, not encrypted)
     * @param signature the signature for the data, signed with the private key
     * @throws IOException if encoding algorithm is not supported or key specification
     * is invalid
     */
    public static boolean verifyPurchase(String base64PublicKey, String signedData,
                                         String signature) throws IOException {
        if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey)
                || TextUtils.isEmpty(signature)) {
             //Purchase verification failed: missing data
            return false;
        }

        PublicKey key = generatePublicKey(base64PublicKey);
        return verify(key, signedData, signature);
    }

    /**
     * Generates a PublicKey instance from a string containing the Base64-encoded public key.
     *
     * @param encodedPublicKey Base64-encoded public key
     * @throws IOException if encoding algorithm is not supported or key specification
     * is invalid
     */
    public static PublicKey generatePublicKey(String encodedPublicKey) throws IOException {
        try {
            byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
        } catch (NoSuchAlgorithmException e) {
            // "RSA" is guaranteed to be available.
            throw new RuntimeException(e);
        } catch (InvalidKeySpecException e) {
            String msg = "Invalid key specification: " + e;
            throw new IOException(msg);
        }
    }

    /**
     * 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) {
        byte[] signatureBytes;
        try {
            signatureBytes = Base64.decode(signature, Base64.DEFAULT);
        } catch (IllegalArgumentException e) {
           //Base64 decoding failed
            return false;
        }
        try {
            Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM);
            signatureAlgorithm.initVerify(publicKey);
            signatureAlgorithm.update(signedData.getBytes());
            if (!signatureAlgorithm.verify(signatureBytes)) {
               //Signature verification failed
                return false;
            }
            return true;
        } catch (NoSuchAlgorithmException e) {
            // "RSA" is guaranteed to be available
            throw new RuntimeException(e);
        } catch (InvalidKeyException e) {
           //Invalid key specification
        } catch (SignatureException e) {
            //Signature exception
        }
        return false;
    }
}

6 Run and Test the application

Now Run your app and test in app purchase by using your gmail account that is associated with your Google Console developer account. Because by default, the only test account registered is the one that’s associated with your developer account. You can register additional test accounts by using the Google Play Console.

Conclusion

So in this article we have learnt how to integrate and make in app purchase of one time product. If you liked the article then please share this page and article. Thanks.

NOTE:

If you need personal technical assistance for integrating Google Play Billing Library with latest version than you can book order with me via Fiver here.

References:

https://developer.android.com/google/play/billing/billing_overview

https://developer.android.com/google/play/billing/billing_onetime

https://github.com/android/play-billing-samples/blob/551a178e52baf60cc6e1f9cb6f40767b8453655a/TrivialDrive/app/src/main/java/com/example/android/trivialdrivesample/util/Security.java

Please Subscribe Youtube| Like Facebook | Follow Twitter


Share Post
  •  
  •  
  •  
  • 1
  •  
  •  
  •  
  •  
  •  
  •  
  •  
    1
    Share

119 Replies to “How to make In App Purchase in Android Using Google Play Billing Library”

  1. Thanks for this.
    Given the present Billing Library V4, some parts of the code appears deprecated such as queryPurchases. Please we’ll be glad if you can edit this to reflect the present library.
    Thanks

  2. Dear, Thanks for your tutorial, But i’m facing a issue. after subscription successful not open premium content. I think issue is with shared preferences. Can you help me to solve this issue ?

  3. How can we check for already purchased item ?. I need this code for restore functionality. By the way your code is awesome, I am searching from last month for billing library 4.0.0 and finally got your tutorial.

    1. here it is
      //if item already purchased then check and reflect changes
      else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {

      And

      I have not done yet for version 4.
      you can book order for personal support and implementation with latest library via Fiver.
      https://www.fiverr.com/share/NEW6eN
      Thanks

  4. Hey your code is extremely good, i have question about when i can add data on backend as well after purchase the item, in old code i add when “IabHelper.OnConsumeFinishedListener” in these method i make function like these

    “addOrder(price, “inapp”, “paid”, purchase.getOrderId(), “”, currency_code);”

    so in new code where can i add these line for add data like these after item purchase

    Thank you!

    1. here i think you can get purchase infoe inside purchase object
      if ( purchase.getSkus().contains(PRODUCT_ID) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
      //purchased object

  5. if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
    // Invalid purchase
    // show error to user
    Toast.makeText(getApplicationContext(), “Error : Invalid Purchase”, Toast.LENGTH_SHORT).show();
    return;
    }

    Purchase is shown successful but verification side problem.
    my Code stops here Giving Toast “Error : Invalid Purchase”, can you help me out this.

  6. Hey, thank you very much. I was wondering if we can use Preferences DataStore instead of SharedPrefences for storing purchases?

  7. Thanks, This guidance is really helpful.
    I’ve got a question.
    Using your method, I got 10 Order, but only one of them successfull. I got these messages for the unseccssfull payment “There was an issue charging the customer’s payment method ~ Cancelling”.
    Any suggesting, Thanks before

    Ps: Sorry if my english is bad

  8. Hello: I changed the test device android 6.0 to android 8.0 device, it can work normally, if anyone encounters the same problem, you can refer to it ! Thank you !

  9. Thanks for the tutorial, which provides a step-to-step guidance for implementation of in-app purchase. It is really helpful. I have a question regarding testing and debugging. I am currently working with Android studio. Can I test it with android studio, or I have to do it in Google Play Console. If it must be the latter, do I have to run the code in signed release APK? Thanks.

    1. Yes you can test it with android studio. But first you have to release signed apk in internal test track in play console with billing permission added in manifest.

      thanks

  10. Hello, thank for your tutorial, It work for me.
    But I still have a problem,
    I do a project with many activity and ad, I hope when I buy product, ad will disappear.
    so ,I have to get record form billingclient,
    In other page ,how can I get billingclient record?
    or I have to set a global SharedPreferences to get value?
    thanks.

    1. yes through SharedPreferences .

      On every app start get record from billingclient and save/update in SharedPreferences then in every activtity check status from SharedPreferences.
      thanks

  11. Hello

    Great tutorial. I had been trying for months to integrate inApp purchases and couldn’t get it to work. The Android documentation was driving me crazy. great job!

    Let’s see, I followed step by step the tutorial and testing in the emulator with the sku “android.test.purchased” everything works great, consumable and non-consumable purchases. So I decided to upload to private the apk signed with my id’s and my key to be able to do a test.

    I have the different products configured and activated, I have the apk signed and published in private, I have a list of testers and one of the emails is my own developer email.

    I downloaded the apk from the store and it’s been a week since then, and I always get the same message “Error the item you requested is not for sale”.

    I don’t know what I might be missing. I would appreciate your help

    Regards

    **Apologies for my English

    1. You need to make sure:
      * Make sure to upload the signed APK to developer console.
      * Make sure to install the signed APK on your device not launch the app in the debugger.
      * Make sure to create a test account in your developer console.
      * Setup you testing account
      * Make sure to sign in your device with your test account.
      * In a case of closed alpha/beta testing, make sure you have added your test account to selected testers group, you can do this on the page of management your alpha/beta version.
      * In a case of closed alpha/beta testing, make sure your testing account have accepted participation in testing of this application via special invite link
      * Make sure to create in app billing in your developer console and finally activate the item from the console!!! (this is the one that got me after fully following google’s tutorial)
      * Make sure to set VersionCode and VersionName in the manifest to be the same as the version in the developer console (Alpha, Beta or Production. Drafts does not work anymore).
      * Make sure product id in play console and product id in your code is same and matched.
      * Make sure to clear data and cache of your playstore app on your phone to see if fixed. If not then wait for one day for item to be available
      reference: https://stackoverflow.com/questions/13117081/the-item-you-requested-is-not-available-for-purchase

      1. Hello

        thanks for your answer, I have gone through everything you comment and it is apparently all ok, but I keep getting null in “onPurchasesUpdated” in the list. If I run the code with the test id (android.test.purchased) everything goes perfectly, but when I put my IDs I always get the same, null the message of “this article is not for sale”

        don’t know what else I can do. I have configured the id in my console and they are active, the apk is published in the test channel. I have installed from Google Play with the qr that the console gives me …

        I would appreciate any other indication

        Thank you

        1. I have installed from Google Play with the qr that the console gives me ? You can use app from android studio directly by clicking on run in android studio and test your app in app purchase.

          and make sure version code of your published signed apk is same as currently used in source code for testing app.

          furthermore billing permission added.

          And finally you are using same gmail account that is used for your play console account.

          thanks

  12. Hey thanks for this great tutorial!

    One question…
    In onBillingSetupFinished when I call billingClient.queryPurchases I always get null. Anything else works just fine. I can purchase and the text shows purchased. After the next start of the App queryPurchases gives me null value and I have to purchase again. But then I get an error showing “you already own this product”. This happens on my developer devices. Any advice what I can look for?

  13. Thank you so much for this IAP tutorials!

    But I have a problem with this approach.. getPurchasesList always returns null.

    When I first purchase a product everything works fine. But when onCreate is being called the next time after the purchase getPurchasesList returns null even if the purchase was successful. Then I try to purchase again and get “you already own this item”-Error.

    When does getPurchasesList return NULL?

    1. if product is consumable then after purchase it will be consumed and purchase will be empty/null and in case of non consumable you need to make sure you have acknowledged it properly.
      please check your developer’s public key of app is correct.

      And getPurchasesList returns null when you have purchased consumable product or you have not bought anything yet.

  14. hi, when using lucky patcher the purchase getting cracked, so could you please update this tutorial for verify in server.

    1. I do not have tried server side verification.
      To prevent purchase/subscription from hacking we can use following measure
      1. perform client side verification which is mentioned in video.
      2. perform server side verification, see info here https://developer.android.com/google/play/billing/security

      However above two step still not guaranteed for 100% security because hacker can reverse engineer your apk and bypass verification check then redistribute it, unless your app is depend on online service where only authorized users get your content by performing authentication check at your own backend server.

      Moreover you can also more secure your apk from reverse engineering by performing obfuscation on your apk (allows you to make it difficult for hackers to modify/crack your apk).
      see here
      https://programtown.com/how-to-shrink-obfuscate-and-optimize-android-apk/

      thank you

  15. Thanks so much it helps a lot!
    I was wondering, how do you get the price set online so that I can display it on the purchase button?

  16. Please I keep getting this response email
    (This test purchase was cancelled because it was not acknowledged. You should ensure all purchases are acknowledged, so they are not subject to refunds)
    please any help or I’m doing something wrong

  17. I have a problems with part of code (when app starts) which checks the purchase status.

    I always get (even after properly buying and acknowledging purchase) DEVELOPER_ERROR whenever calling:

    Purchase.PurchasesResult alreadyPurchasedResult = billingClient.queryPurchases(item.getSkuId());

    Why?

    1. DEVELOPER_ERROR
      Invalid arguments provided to the API. This error can also indicate that the application was not correctly signed or properly set up for In-app Billing in Google Play, or does not have the necessary permissions in its manifest.

      Also check this link
      And this link

      1. Actually I solved it – it was fault on my side. As a parameter of queryPurchases should NOT be skuID as I was using, but BillingClient.SkuType.INAPP/SUBS. Such a rookie mistake, but took me couple hours to figure out 🙂

  18. Hello,

    I was trying this article . But I am getting error:

    “Purchase Item not Found”

    It looks like that onSkuDetailsResponse always returns an empty list although I have created a product ID named ‘purchase’ in the play console.

    Could you please help me fix this?

    1. Below are different ways of fixing it.
      Please try to clear data and cache of your playstore app on your phone to see if fixed
      If not then wait for one day for item to be available

      Check Here For info
      https://stackoverflow.com/questions/62320895/android-google-pay-billing-response-4-item-unavailable

      Furthermore make sure app is uploaded on playstore in any track with billing permission added and product id in your Play console is same as product id used in your app project.
      Thanks

  19. No error message appears, I press the button and nothing happens. The application is published in Google Play and tested with a real phone.

  20. I fixed the problem, I had not entered the key Base64.

    But now something else happened, I created a new product ID, and the price window does not open

    1. What error message you are receiving?
      Please make sure product id in playstore and in your app is same.
      Furthermore
      Please test with real phone and add billing permission in manifest and upload signed apk with new version code in playstore in test track.
      And then wait when your app is published in test track then test it.
      please check below link for further troubleshoot.

      https://stackoverflow.com/questions/11068686/this-version-of-the-application-is-not-configured-for-billing-through-google-pla

      Thanks

      1. No error message appears, I press the button and nothing happens. The application is published in Google Play and tested with a real phone

  21. I did everything as described and it works, but after 5 minutes the test purchase is canceled and I don’t know why. “This test purchase was canceled because it was not confirmed”.

  22. I did everything as described and it works, but after 5 minutes the test purchase is canceled and I don’t know why.

    “This test purchase was canceled because it was not confirmed”.

    void handlePurchases(List purchases) {
    for(Purchase purchase:purchases) {
    //if item is purchased
    if (PRODUCT_ID.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
    {
    if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
    // Invalid purchase
    // show error to user
    Toast.makeText(getApplicationContext(), “Error : Invalid Purchase”, Toast.LENGTH_SHORT).show();
    return;
    }
    // else purchase is valid
    //if item is purchased and not acknowledged
    if (!purchase.isAcknowledged()) {
    AcknowledgePurchaseParams acknowledgePurchaseParams =
    AcknowledgePurchaseParams.newBuilder()
    .setPurchaseToken(purchase.getPurchaseToken())
    .build();
    billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase);
    }
    //else item is purchased and also acknowledged
    else {
    // Grant entitlement to the user on item purchase
    // restart activity
    if(!getPurchaseValueFromPref()){
    savePurchaseValueToPref(true);
    Toast.makeText(getApplicationContext(), “Item Purchased”, Toast.LENGTH_SHORT).show();
    this.recreate();
    }
    }
    }

  23. Hello Sir,
    I have a problem. When I clicked Purchase button , it says ” This version of the application is not configured for billing through Google Play”.
    I followed instructions and applied everything you did.
    What should I do to fix this? Can you help me? Thanks.

    1. Hi
      Please test with real phone and add billing permission in manifest and upload signed apk with new version code in playstore in test track.
      And then wait when your app is published in test track then test it.
      please check below link for further troubleshoot.
      Click here
      Thanks

          1. Hey I have another problem. I want to add a few more non-consumable purchase items to my activity. Which code sections should I duplicate and change?
            By the way, I found this website via your youtube video, you are very helpful.

  24. Hello, first thank you very much for the tutorial, it helped me a lot, I was racking my brain with code that I was building from scratch and when I saw this tutorial I managed to see my errors. I have a question about consumable products, I changed my code to the one below, but when I test it, it shows that I already bought the product, what can it be? Kind regards.

    void handlePurchases(List purchases) {
    for(Purchase purchase:purchases) {

    ConsumeParams consumeParams =
    ConsumeParams.newBuilder()
    .setPurchaseToken(purchase.getPurchaseToken())
    .build();

    billingClient.consumeAsync(consumeParams, listener);

    }
    }

    1. Thank you

      You are reading article of Single Non-consumable one-time product.(that allows user to purchase product only once)

      For Single Consumable one-time product (that allows user to purchase single product again ) (Follow this link)

      For Multiple Consumable one-time product (that allows user to purchase multiple products again ) (Follow this link)

      When product is bought for consumable product then you need to consume it after purchasing it so that product can be bought again.
      You can look up above tutorials if any help needed then feel free to ask.

      Further more in your code. You are consuming every product regardless of its status i.e purchased, pending, unspecified.
      You need to consume only those product which has been purchased. Then you have to consume it after purchase. My mentioned
      links clearly explain how to perform purchase of single and multiple Consumable products.

      thanks

      1. Thank you very much, I did your tutorial and it worked. I am working dynamically. Taking items from an api and the user consuming in real time.

  25. Hi ProgramTown for your very useful tutorial,actually this is only broadly description based tutorial for this purpose.
    You posted for non consumable and subscription based tutorial as i learned both using acknowlegePurchase.
    Where i can find out similar tutorial for consumable item because after using consumeAsyc i found out purchase cancelled email from Google play for not doing acknowlegde. So it makes me confused what to do.
    And current version is 3.0.0 what i am using.

    1. For consumables item after verifying purchase run only ConsumeResponseListener.
      acknowledgePurchase is not required.
      
      From google doc 
      
      For consumables, the consumeAsync() method fulfills the acknowledgement requirement and indicates 
      that your app has granted entitlement to the user. This method also enables your app to make the 
      one-time product available for purchase again.
      
      To indicate that a one-time product has been consumed, call consumeAsync()
      and include the purchase token that Google Play should make available for repurchase.
      You must also pass an object that implements the ConsumeResponseListener interface. 
      This object handles the result of the consumption operation. You can override the onConsumeResponse() method, 
      which the Google Play Billing Library calls when the operation is complete.
      
      The following example illustrates consuming a product using the associated purchase token:
      
      void handlePurchase(Purchase purchase) {
          // Purchase retrieved from BillingClient#queryPurchases or your PurchasesUpdatedListener.
          Purchase purchase = ...;
      
          // Verify the purchase.
          // Ensure entitlement was not already granted for this purchaseToken.
          // Grant entitlement to the user.
      
          ConsumeParams consumeParams =
              ConsumeParams.newBuilder()
                  .setPurchaseToken(purchase.getPurchaseToken())
                  .build();
      
          ConsumeResponseListener listener = new ConsumeResponseListener() {
              @Override
              public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
                  if (billingResult.getResponseCode() == BillingResponseCode.OK) {
                      // Handle the success of the consume operation.
                  }
              }
          };
      
          billingClient.consumeAsync(consumeParams, listener);
      }

      So Just change AcknowledgePurchaseResponseListener to ConsumeResponseListener for consuming as well as
      acknowledging one time consumable product. Reply if you have any issues?

      1. Thank you Sir for your reply.
        You used AcknowledgePurchaseListener once after checking purchase acknowleged or not. So here the replacement

        if (!purchase.isAcknowledged()) {
        ConsumeParams consumeParams = ConsumeParams.newBuilder()
        .setPurchaseToken(purchase.getPurchaseToken())
        .build();

        ConsumeResponseListener listener = new ConsumeResponseListener() {
        @Override
        public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
        // Toast.makeText(PurchaseWindow.this,”Successfully purchased and Acknowleged”,Toast.LENGTH_LONG).show();
        }
        }
        };

        billingClient.consumeAsync(consumeParams, listener);
        }

        Please correct me if i did any wrong.

        1. yes you have done it right.
          
          just add listener outside the method (I just prefer this)
          i.e
          if (!purchase.isAcknowledged()) {
          	ConsumeParams consumeParams = ConsumeParams.newBuilder()
          	.setPurchaseToken(purchase.getPurchaseToken())
          	.build();
          
          	billingClient.consumeAsync(consumeParams, listener);
          }
          ....
          } 
          //handlePurchases method end
          //outside handlePurchases() method
          ConsumeResponseListener listener = new ConsumeResponseListener() {
          	@Override
          	public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
          		if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
          		// Toast.makeText(PurchaseWindow.this,”Successfully purchased and Acknowleged”,Toast.LENGTH_LONG).show();
          		}
          	}
          };
      2. i put ConsumeResponselistener within handlepurchase() as i followed google doc.
        I also wanna know which purchase item pass this toast and customer may get OrderId too,it may handy when multiple items are in pending and acknowledged one by one,


        ConsumeResponseListener listener = new ConsumeResponseListener() {
        @Override
        public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
        Toast.makeText(PurchaseWindow.this,"Successful And Acknowledged : "+purchase.getSku()+"\n"+purchase.getOrderId(),Toast.LENGTH_LONG).show();
        }
        }
        };

        1. onConsumeResponse method only accept puchaseToken as arguments so there is no way of knowing which purchase item
          is being consumed and acknowledged. Only token info is passed. 
          Further more listener will be called asynchronously for each item one by one when multiple items are being consumed.
  26. Hi! Great tutorial!

    Do I have to run AcknowledgePurchaseResponseListener and ConsumeResponseListener both for consummables?

    1. For consumables run only  ConsumeResponseListener.
      
      From google doc 
      
      For consumables, the consumeAsync() method fulfills the acknowledgement requirement and indicates 
      that your app has granted entitlement to the user. This method also enables your app to make the 
      one-time product available for purchase again.
      
      To indicate that a one-time product has been consumed, call consumeAsync()
      and include the purchase token that Google Play should make available for repurchase.
      You must also pass an object that implements the ConsumeResponseListener interface. 
      This object handles the result of the consumption operation. You can override the onConsumeResponse() method, 
      which the Google Play Billing Library calls when the operation is complete.
      
      The following example illustrates consuming a product using the associated purchase token:
      
      void handlePurchase(Purchase purchase) {
          // Purchase retrieved from BillingClient#queryPurchases or your PurchasesUpdatedListener.
          Purchase purchase = ...;
      
          // Verify the purchase.
          // Ensure entitlement was not already granted for this purchaseToken.
          // Grant entitlement to the user.
      
          ConsumeParams consumeParams =
              ConsumeParams.newBuilder()
                  .setPurchaseToken(purchase.getPurchaseToken())
                  .build();
      
          ConsumeResponseListener listener = new ConsumeResponseListener() {
              @Override
              public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
                  if (billingResult.getResponseCode() == BillingResponseCode.OK) {
                      // Handle the success of the consume operation.
                  }
              }
          };
      
          billingClient.consumeAsync(consumeParams, listener);
      }
    1. In this article google play billing library version ‘com.android.billingclient:billing:2.2.0’
      is used which includes BillingHelper Class.
      In latest library BillingHelper Class is removed thats why you are facing issue.

      BillingHelper Class (import com.android.billingclient.util.BillingHelper)
      is only used in Security.java class.

      Which is simply printing warning log using
      i.e BillingHelper.logWarn(TAG, “Purchase verification failed: missing data.”);

      So simply Either remove
      import com.android.billingclient.util.BillingHelper
      and BillingHelper.logWarn() method calls from Security.java class in your project
      OR use google play billing library version ‘com.android.billingclient:billing:2.2.0’

          1. Hello there, it’s me again. I followed your tutorial and it has worked perfectly, but now I want to add more one time products. I have 31 of them, so how do I got about it using this tutorial?

          2. For multiple purchase items you have to make arraylist of purchase item ids and 
            loop through purchase list and make neccessary changes below is sample code with 4 one time product items.
            
            
            Note: Code is not tested but this is acheived through this approach 
            Also Note: Make preference key and product item id same. and then check preference 
            for specied product item to show or hide feautures.

            Code:

            //note add unique product ids
            //use same id for preference key
            private static ArrayList purchaseItemIDs = new ArrayList() {{
            		add("purchaseitem1");
            		add("purchaseitem2");
            		add("purchaseitem3");
            		add("purchaseitem4");
            }};
            
            private boolean getPurchaseItemValueFromPref(String purchaseItem){
            	return getPreferenceObject().getBoolean(purchaseItem,false);
            }
            private void savePurchaseItemValueToPref(String purchaseItem,boolean value){
            	getPreferenceEditObject().putBoolean(purchaseItem,value).commit();
            }
            
            //Inside oncreate method
            billingClient.startConnection(new BillingClientStateListener() {
            	@Override
            	public void onBillingSetupFinished(BillingResult billingResult) {
            		if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
            			Purchase.PurchasesResult queryPurchase = billingClient.queryPurchases(INAPP);
            			List queryPurchases = queryPurchase.getPurchasesList();
            			if(queryPurchases!=null && queryPurchases.size()>0){
            				handlePurchases(queryPurchases);
            			}
            
            			//check which items are in purchase list and which are not in it
            			//if items that are found add them to purchaseFound
            			//check status of found items and save values to preference
            			//item which are not found simply save false values to their preference
            			//indexOf return index of item in purchase list from 0-3 (because we have 4 items) else returns -1 if not found
            			
            			ArrayList purchaseFound =new ArrayList ();
            			if(queryPurchases!=null && queryPurchases.size()>0){
            				   //check item in purchase list
            				   for(Purchase p:queryPurchases){
            					   int index=purchaseItemIDs.indexOf(p.getSku());
            					   //if purchase found
            					   if(index>-1)
            					   {
            						   purchaseFound.add(index);
            						   if(p.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
            						   {
            							   savePurchaseItemValueToPref(purchaseItemIDs.get(index),true);
            						   }
            						   else{
            							   savePurchaseItemValueToPref(purchaseItemIDs.get(index),false);
            						   }
            					   }
            				   }
            				   //items that are not found in purchase list mark false
            				  //indexOf returns -1 when item is not in foundlist
            				   for(int i=0;i < purchaseItemIDs.size(); i++){
            					   if(purchaseFound.indexOf(i)==-1){
            						   savePurchaseItemValueToPref(purchaseItemIDs.get(i),false);
            					   }
            				   }
            			}
            			//if purchase list is empty that means no item is not purchased
            			//Or purchase is refunded or canceled
            			//so mark them all false
            			else{
            				for( String purchaseItem: purchaseItemIDs ){
            					savePurchaseItemValueToPref(purchaseItem,false);
            				}
            			}
            
            		}
            	}
            
            	@Override
            	public void onBillingServiceDisconnected() {
            	}
            });
            
            //handle purchase for each product item
            void handlePurchases(List  purchases) {
            
            	for(Purchase purchase:purchases) {
            
            	int index=purchaseItemIDs.indexOf(purchase.getSku());
            	//purchase found
            	if(index>-1) {
            
            		//if item is purchased
            		if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
            		{
            			if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
            				// Invalid purchase
            				// show error to user
            				Toast.makeText(getApplicationContext(), "Error : Invalid Purchase", Toast.LENGTH_SHORT).show();
            				continue;//skip current iteration only because other items in purchase list must be checked if present
            			}
            			// else purchase is valid
            			//if item is purchased and not acknowledged
            			if (!purchase.isAcknowledged()) {
            				AcknowledgePurchaseParams acknowledgePurchaseParams =
            						AcknowledgePurchaseParams.newBuilder()
            								.setPurchaseToken(purchase.getPurchaseToken())
            								.build();
            				billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackPurchase);
            			}
            			//else item is purchased and also acknowledged
            			else {
            				// Grant entitlement to the user on item purchase
            				// toast restart activity //because other items in list may needs to be acknowledged
            				if(!getPurchaseItemValueFromPref(purchaseItemIDs.get(index))){
            					savePurchaseItemValueToPref(purchaseItemIDs.get(index),true);
            					Toast.makeText(getApplicationContext(), "Item Purchased. Please restart Activity", Toast.LENGTH_SHORT).show();
            				}
            			}
            		}
            		//if purchase is pending
            		else if(  purchase.getPurchaseState() == Purchase.PurchaseState.PENDING)
            		{
            			Toast.makeText(getApplicationContext(),
            					purchaseItemIDs.get(index)+" Purchase is Pending. Please complete Transaction", Toast.LENGTH_SHORT).show();
            		}
            		//if purchase is refunded or unknown
            		else if( purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE)
            		{
            			savePurchaseItemValueToPref(purchaseItemIDs.get(index),false);
            			Toast.makeText(getApplicationContext(), purchaseItemIDs.get(index)+" Purchase Status Unknown", Toast.LENGTH_SHORT).show();
            		}
            	}
            
            	}
            }
             
            AcknowledgePurchaseResponseListener ackPurchase = new AcknowledgePurchaseResponseListener() {
            	@Override
            	public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
            		if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
            			//if purchase is acknowledged
            			//then it is auto saved in preference later during app restart
            			Toast.makeText(getApplicationContext(), "Item Purchased. Please restart Activity", Toast.LENGTH_SHORT).show();
            		}
            	}
            };
            
            
            //Note use specified particualar id to initiate purchase inside initiatePurchase() method
            //i.e skuList.add(purchaseItemIDs.get(1)); //trying to make purchase request for "purchaseitem2"
          3. Thank you for the response. Will I have to replace all the code form this tutuorial or just add onto it?

          4. Just replace the mentioned comment code and change initiatePurchase() method according to your need.
            Plus create purchase buttons for different button i.e purchaseItem1 , purchaseItem2 e.t.c. or list to initiate Purchase of particular item.
            
            Furthermore read again the article then This(click here) comment.
            You will know what changes needs to be done. 
          5. Thank you for the code. I’ve implemented the billing, but I encountered a few issues.

            I keep getting an error:

            incompatible types: Object cannot be converted to Purchase
            for(Purchase p:queryPurchases){

            It’s given me errors on 2 of these lines.

            savePurchaseItemValueToPref(purchaseItemIDs.get(index),true);

            What did I do wrong?

          6. There may be variable name or typo mistake in your code. Please check my code then review your code and import statements.  
            (incompatible types: Object cannot be converted to Purchase) means in your code queryPurchases is list of 
            object type but actually it should be list of purchase type.
          7. Fixed my problems. The only issue I have left is this.
            Raw use of parameterized class ‘ArrayList’
            Oh, do I also have to add a file for the product IDs or is should a just use the ones I put in the code?

            Sorry If I’m asking too many questions. I’m new to in app billing.

          8. Sorry for this again, but for this:
            public static final String PRODUCT_ID= “purchase”;

            Should I leave it as “purchase” or do I add more product IDs?

          9. public static final String PRODUCT_ID= “purchase”;
            this was used for when you have one time product item. But you said you need 31 one time product items,
            So you need 31 product ids thats why I wrote arraylist of product ids. we do not need PRODUCT_ID string now. 
            Therefore use static arraylist of 31 product ids 
            i.e 
            
            private static ArrayList purchaseItemIDs = new ArrayList() {{
            		add("purchase1");
            		add("purchase2");
            		add("purchase3");
                            .
                            .
            		add("purchase31");
            }};
            
            no need to add file for product ids just declare static arraylist for product ids
            
            One more thing you need to create 31 managed product item on google play console 
            with same name as these productids. 
            
            and it not mandatory to name them it exactly purchase1.....purchase31
            you can use any name for these 31 product item ids.
            
            Thanks
          10. Note: I had to create a database for my images after the last time we emailed, because they would’ve significantly increased app size.

            I understand. Well my products(images) are stored in a Firebase Realtime database server, but I got them to show on a recyclerview. I’ve already setup an OnClick event for each item, so I don’t need to make 31 buttons right?

            Also from what I understand about the public void purchase1(View view) method, I’ll have to make 31 one of those methods right?

          11. Also from what I understand about the public void purchase1(View view) method, I’ll have to make 31 one of those methods right?
            
            yes you have to make 31 method for 31 button item clicks but as i said recycle view would be feasible approach.
            And you have already implemented recycleview for product images then onclick of particular item from list 
            you can initiate purchase when item click get index/position of selected/clicked item so no need for 31 buttons now
            
            index/position 0 for purchase1
            index/position 1 for purchase2
                                      .
                                      .
            index/position 30 for purchase31
            
            here is listener code when recycle view particular item clicked 
            Suppose position variable is integer which defines position of particular product item then
            
                   //check if service is already connected
            
            if (billingClient.isReady()) {
            	initiatePurchase(purchaseItemIDs.get(position));  //note here arraylist of particular product item will be passed based on product id selection from list
            }
            //else reconnect service
            else{
            	billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
            	billingClient.startConnection(new BillingClientStateListener() {
            		@Override
            		public void onBillingSetupFinished(BillingResult billingResult) {
            			if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            				initiatePurchase(purchaseItemIDs.get(position));
            			} else {
            				Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
            			}
            		}
            		@Override
            		public void onBillingServiceDisconnected() {
            		}
            	});
            }	
               
            
            and now in initiate purchase
            
            private void initiatePurchase(String productid) {
                    List skuList = new ArrayList();
                    skuList.add(productid);
                    SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
                    params.setSkusList(skuList).setType(INAPP);
                    billingClient.querySkuDetailsAsync(params.build(),
                            new SkuDetailsResponseListener() {
                                @Override
                                public void onSkuDetailsResponse(BillingResult billingResult,
                                                                 List skuDetailsList) {
                                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                                        if (skuDetailsList != null && skuDetailsList.size() > 0) {
                                            BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                                                    .setSkuDetails(skuDetailsList.get(0))
                                                    .build();
                                            billingClient.launchBillingFlow(MainActivity.this, flowParams);
                                        }
                                        else{
                                            //try to add item/product id "purchase" inside managed product in google play console
                                            Toast.makeText(getApplicationContext(),"Purchase Item not Found",Toast.LENGTH_SHORT).show();
                                        }
                                    } else {
                                        Toast.makeText(getApplicationContext(),
                                                " Error "+billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
                                    }
                                }
                            });
                }
            
            Note: using recycleview now you can remove 31 buttons from screen and purchase1(View view) methods
            Further Note: why you are using realtime db for images if you have 31 images for each product
            and each image size is 500kb then total 16 MB image size will not effect app size you can store it in drawable or
            raw folder. But it depend on your strategy how much important factor app size is for you. 
          12. Thank you again. The reason I’m using a database is because I don’t know how to right the code to download images from app drawable, so that users can download the image once purchased. Plus my app is currently 10MB in size and the images are 27Mb in size altogether, but I might change my strategy again, because your suggestion sounds a lot easier.

            Well I think I’ve asked every question I could’ve for now. If I have issues I’ll come back, but I’ll also tell you when this finally works.
            Thank you for your help once again

          13. Oh I thought image was for product showcase i.e product view image but your product is actually an image.
            So for that real time db is right approach because once image is purchased user will have to download that image
            from firebase real time db server.
            putting image inside drawable or raw can be risky because hacker can crack your apk and extract those images from apk.
            For that realtime db server is best for security
          14. Yes. That’s why I did it originally, but I’ve been struggling to implement this with in-app billing. I think I might go with storing the images in the drawable for now, because I do want to earn a living, but I’ll switch to the database later on when my user downloads increase.

            Oh I also wanted to ask how to turn this testing in app billing into a live in app billing?

          15. yes for now you can use drawable or raw folder.
            
            Oh I also wanted to ask how to turn this testing in app billing into a live in app billing?
            For In app live just create 31 product items in googleplay console with same ids, set prices and description
            and then publish your app on playstore in production track and do not forget to paste API string in your app for
            purchase verification.
            i.e
            // To get key go to Developer Console > Select your app > Development Tools > Services & APIs.
            String base64Key = "Add Your Key Here"
          16. Hey there. I just wanted to tell you that I’ve found a better to sell my images. Now I’m telling you this, because I wanted to make sure that what I do what give me or my users issues.

            So now instead of making a recyclerview where users can individually purchase items with a button click.
            I’ve decided to turn the recyclerview into a list of packages that includes multiple images. When they click this package it take them to an activity that displays the images and there that’s where they can buy those images.
            Now I was thinking that I would create Product IDs with the package name, that the purchase is actually for the package and not the image itself and once they purchase the package a download button appears and they are able to download them.
            I’m planning on using your original tutorial and just make multiple activities for those packages. It would be an issue, because I won’t have too many packages.

            I’m confident it’ll work, but I just want your feedback on things that I should or should not do, because it would give me and users a problem.

          17. just use single Security class and call its static method Security.verifyPurchase(base64Key, signedData, signature)
            from anywhere to validate purchase
          18. Hey man. I’m suddenly having trouble with my in app billing. I published my app on an Internal testing track and tried to test it, but I keep getting a “Item not found” dialog, when I check the Pre-launch report devices, the billing client works perfectly.

            I want to be able to test purchase, because a different button is supposed to show up when they complete purchases, but I can’t know if this works, because the billing flow doesn’t appear. Plus for some reason the testers don’t buy the products even though it’s a test.

            Thank you

          19. The billing works. Thank you, but now I have another problem.

            I have 4 in-app products which can be purchased in 4 different activities. Now when I purchase one product from one activity the other 3 three show that they’ve also been purchased.

        1. Oh. I understand now.
          So I should also add the IDs after here

          private void initiatePurchase() {
          List skuList = new ArrayList();
          skuList.add(“PRODUCT_ID”);

          and what do I replace the “PRODUCT_ID” string with, because now I’m receiving this:
          Cannot resolve symbol ‘PRODUCT_ID’

          Thank you. Once again sorry for asking too many questions

          1. private void initiatePurchase() {
            List skuList = new ArrayList();
            skuList.add(“PRODUCT_ID”);
            
            As you have 31 product ids we have to make initiatePurchase for 31 items.
            initiatePurchase() previously was initiating purchase of  single product item "purchase".
            now we have to initiate purchase of 31 product items so replace 
            skuList.add(“PRODUCT_ID”); with your specified purchase of particular item
            
            for that add 31 buttons for 31 product for purchasing or use listview/recycle list of 31 items
            then on particular item click initiate purchase of that particular item.
            
            Note: listview/recycle list is preferred because putting 31 purchase item buttons on screen is bad
            here is sample for buttons for understanding how purchase of 31 items handled
            i.e on user screen will be
            click to purchase item 1 
            click to purchase item 2
                                   .
                                   .
            click to purchase item 31 
            
            xml code: note here only two button added to purchase purchase1 and purchase31 item only for simplicity
            And please add button opening closing tag to below both button because here in comment section xml tags are not supported
            
            android:id="@+id/purchase_button1"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="click to purchase item 1"
                    android:onClick="purchase1"
                
                    android:id="@+id/purchase_button2"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="click to purchase item 31"
                    android:onClick="purchase31"
            when particular button press or list item selected then on event call initiate purchase of given item
            then on java code side onclick button event of particular item initiate purchase of given item
            
            //initiate purchase of product item 1 on  purchase1 button click
            public void purchase1(View view) {
            	//check if service is already connected
            	if (billingClient.isReady()) {
            		initiatePurchase("purchase1");  //note in arraylist 1st product item key was purchase1
            	}
            	//else reconnect service
            	else{
            		billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
            		billingClient.startConnection(new BillingClientStateListener() {
            			@Override
            			public void onBillingSetupFinished(BillingResult billingResult) {
            				if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            					initiatePurchase("purchase1");
            				} else {
            					Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
            				}
            			}
            			@Override
            			public void onBillingServiceDisconnected() {
            			}
            		});
            	}
            }
            
            //initiate purchase of 31 product item on  purchase31 button click
            public void purchase31(View view) {
            	//check if service is already connected
            	if (billingClient.isReady()) {
            		initiatePurchase("purchase31");  //note in arraylist last product item key was purchase31
            	}
            	//else reconnect service
            	else{
            		billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build();
            		billingClient.startConnection(new BillingClientStateListener() {
            			@Override
            			public void onBillingSetupFinished(BillingResult billingResult) {
            				if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            					initiatePurchase("purchase31");
            				} else {
            					Toast.makeText(getApplicationContext(),"Error "+billingResult.getDebugMessage(),Toast.LENGTH_SHORT).show();
            				}
            			}
            			@Override
            			public void onBillingServiceDisconnected() {
            			}
            		});
            	}
            }
            
            now our initiatePurchase() method will look like
            
            private void initiatePurchase(String productid) {
                    List skuList = new ArrayList<>();
                    skuList.add(productid);
                    SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
                    params.setSkusList(skuList).setType(INAPP);
                    billingClient.querySkuDetailsAsync(params.build(),
                            new SkuDetailsResponseListener() {
                                @Override
                                public void onSkuDetailsResponse(BillingResult billingResult,
                                                                 List skuDetailsList) {
                                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                                        if (skuDetailsList != null && skuDetailsList.size() > 0) {
                                            BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                                                    .setSkuDetails(skuDetailsList.get(0))
                                                    .build();
                                            billingClient.launchBillingFlow(MainActivity.this, flowParams);
                                        }
                                        else{
                                            //try to add item/product id "purchase" inside managed product in google play console
                                            Toast.makeText(getApplicationContext(),"Purchase Item not Found",Toast.LENGTH_SHORT).show();
                                        }
                                    } else {
                                        Toast.makeText(getApplicationContext(),
                                                " Error "+billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
                                    }
                                }
                            });
                }
            
            Note: Code needs optimization but for your understanding I have made it simple. and it show purchase of 2 
            product items i.e purchase1 and purchase31 for rest you have to follow same approach.
            
            thanks
  27. One more doubt is that can I test the purchase in an internal test track? I have read that purchases test can be done only on closed/open test tracks. Please let me know this as well. TIA

    1. Yes you can test in app purchase item in internal test track. To test in app purchase item in your app, you first need to Integrate Google Play Library and add billing permission in your app and then upload that apk on Google play console and publish it any track (Internal test track (recommended), Closed track, Open track, Production track). I have already mentioned it at Note section after Requirements heading.

  28. Hi, a wonderful post. Thank you. Is it the same code for the subscription as well? Should I just make INAPP to SUBS?

  29. Thanks, This guidance is really helpful.
    I’ve got a question.
    What is the parameter ‘View view’ for in the definition of
    method purchase…”public void purchase(View view)”
    It looks no need to pass that argument.
    Is there any reason to use it?

    1. I’ve just found that purchaseButton in .xml file has the attribute of ‘android:onClick=”purchase”‘.
      Sorry for my rash question.

  30. Thanks for the step by step guidance.

    In section 5 you write to include the Billing library and permission. If your guide is followed step by step the reader would already added it in section 1. So no need to do it again.

    I would also refer where you have that “Security.java” class from. I assume it’s from the Android billing sample project here: https://github.com/android/play-billing-samples/blob/551a178e52baf60cc6e1f9cb6f40767b8453655a/TrivialDrive/app/src/main/java/com/example/android/trivialdrivesample/util/Security.java

    1. Thanks for pointing out.

      I have repeated in section 5 because some times users directly jump to code section, when they are looking for solution quickly . Now I have created anchor link as remainder if they have skipped section 1.

      For “Security.java” class, license info is included with in class file. By the way now I have added source url in reference section as well.

Leave a Reply

Your email address will not be published. Required fields are marked *