Implement Google Play Billing Library version 2

Thanks @Webfreak, your answer for Kotlin guided to me to the right direction.

Here is how I implemented it for Java:

First add the 'billingclient' library to gradle :

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

And add the required permissions in the manifest file:

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

The Activity must implements the following interfaces:

public class MainActivity extends AppCompatActivity implements
        ...
        PurchasesUpdatedListener,
        AcknowledgePurchaseResponseListener {

Then I initialize the billing client inside the onCreate method:

/** IN-APPS PURCHASE */
    private BillingClient mBillingClient;
    private long mLastPurchaseClickTime = 0;
    private List<String> mSkuList = new ArrayList<>();
    private List<SkuDetails> mSkuDetailsList = new ArrayList<>();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // AppPrefs is just a standalone class I used to get or set shared preferences easily
        mPrefs = AppPrefs.getInstance(this);


        // Rest of your code ...


        /** IN-APP PURCHASES */
        // Initialize the list of all the in-app product IDs I use for this app
        mSkuList.add(Parameters.UNIT_P1);// NoAdsPurchased
        mSkuList.add(Parameters.UNIT_P2);// CustomizationPurchased
        mSkuList.add(Parameters.UNIT_P3);// ChartsPurchased

        // Initialize the billing client
        setupBillingClient();

        // Apply the upgrades on my app according to the user's purchases
        applyUpgrades();
    }

The method setting up the billing client is here, along with the metyhod I use to retrieve the avaialble in-app products fro the app:

private void setupBillingClient() {
        mBillingClient = BillingClient
                .newBuilder(MainActivity.this)
                .enablePendingPurchases() // Useful for physical stores
                .setListener(MainActivity.this)
                .build();

        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    // Load the available products related to the app from Google Play
                    getAvailableProducts();

                    Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);// Or SkuType.SUBS if subscriptions

                    // Init all the purchases to false in the shared preferences (security prevention)
                    mPrefs.setNoAdsPurchased(false);
                    mPrefs.setCustomizationPurchased(false);
                    mPrefs.setChartsPurchased(false);

                    // Retrieve and loop all the purchases done by the user
                    // Update all the boolean related to the purchases done in the shared preferences
                    if (purchasesResult.getPurchasesList() != null) {
                        for (Purchase purchase : purchasesResult.getPurchasesList()) {
                            if (purchase.isAcknowledged()) {
                                Log.e(TAG, purchase.getSku());

                                switch (purchase.getSku()) {
                                    case Parameters.UNIT_P1:
                                        mPrefs.setNoAdsPurchased(true);
                                        break;
                                    case Parameters.UNIT_P2:
                                        mPrefs.setCustomizationPurchased(true);
                                        break;
                                    case Parameters.UNIT_P3:
                                        mPrefs.setChartsPurchased(true);
                                        break;
                                }
                            }
                        }
                    }
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
                // TODO Note: It's strongly recommended that you implement your own connection retry policy and override the onBillingServiceDisconnected() method. Make sure you maintain the BillingClient connection when executing any methods.
                Log.e(TAG, "onBillingServiceDisconnected");
            }
        });
    }

    private void getAvailableProducts() {
        if (mBillingClient.isReady()) {
            SkuDetailsParams params = SkuDetailsParams
                    .newBuilder()
                    .setSkusList(mSkuList)
                    .setType(BillingClient.SkuType.INAPP)
                    .build();

            mBillingClient.querySkuDetailsAsync(params, new SkuDetailsResponseListener() {
                @Override
                public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        mSkuDetailsList = skuDetailsList;
                    }
                }
            });
        }
    }

When a purchase is done by the user (I allow purchases on several Fragments in my app), I call this function on the main Activity (using an interface):

@Override
    public void purchase(String sku) {
        // Mis-clicking prevention, using threshold of 3 seconds
        if (SystemClock.elapsedRealtime() - mLastPurchaseClickTime < 3000){
            Log.d(TAG, "Purchase click cancelled");
            return;
        }
        mLastPurchaseClickTime = SystemClock.elapsedRealtime();

        // Retrieve the SKU details
        for (SkuDetails skuDetails : mSkuDetailsList) {
            // Find the right SKU
            if (sku.equals(skuDetails.getSku())) {
                BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                        .setSkuDetails(skuDetails)
                        .build();
                mBillingClient.launchBillingFlow(MainActivity.this, flowParams);
                break;
            }
        }
    }

Here I implement the methods inherited:

@Override
    public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            for (Purchase purchase : purchases) {
                handlePurchase(purchase);
            }
        } else {
            displayError(R.string.inapp_purchase_problem, billingResult.getResponseCode());
        }
    }

    private void handlePurchase(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            // Grant entitlement to the user.
            applyPurchase(purchase);

            // Acknowledge the purchase if it hasn't already been acknowledged.
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, MainActivity.this);
            }
        }
    }

    @Override
    public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            displayError(R.string.inapp_purchase_success, billingResult.getResponseCode());
        }
    }

The method I added to acknowledge a purchase on my app:

private void applyPurchase(Purchase purchase) {

        switch (purchase.getSku()) {
            case Parameters.UNIT_P1:
                mPrefs.setNoAdsPurchased(true);
                break;
            case Parameters.UNIT_P2:
                mPrefs.setCustomizationPurchased(true);
                break;
            case Parameters.UNIT_P3:
                mPrefs.setChartsPurchased(true);
                break;
        }

        // I remove the ads right away if purchases
        if(mPrefs.getNoAdsPurchased()) {
            destroyAds();
        }
    }

This last method is used to apply all the upgrades/purchases on the app (with an example with the removal of the ads):

private void applyUpgrades() {
        // No ads
        if (mPrefs.getNoAdsPurchased()) {
            destroyAds();
        } else {
            loadAds();
        }

        if (mPrefs.getCustomizationPurchased()) {
            // Allow customization
            // ...
        }

        if (mPrefs.getChartsPurchased()) {
            // Allow charts visualization
            // ...
        }
    }

I guess this solution is not perfect yet but it is working, I will modify the code if I find improvements.


Here is my implementation using billing 2.1.0 in Kotlin. You can easily convert it to Java if you see the whole picture (that's why I'm pasting you the entire activity).

class GoPremiumActivity : AppCompatActivity(), PurchasesUpdatedListener, AcknowledgePurchaseResponseListener {

    private lateinit var billingClient: BillingClient
    private val skuList = listOf(CStr.PRODUCT_ADS_REMOVE.value)
    private var skuDetails: SkuDetails? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.go_premium)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        setupBillingClient()

        purchaseButton.setOnClickListener {
            val flowParams = BillingFlowParams.newBuilder()
                    .setSkuDetails(skuDetails)
                    .build()
            billingClient.launchBillingFlow(this@GoPremiumActivity, flowParams)
        }
    }


    private fun setupBillingClient() {
        billingClient = BillingClient
                .newBuilder(this@GoPremiumActivity)
                .enablePendingPurchases()
                .setListener(this@GoPremiumActivity)
                .build()

        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult?) {
                if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) {
                    getAvailableProducts()

                    val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
                    val purchase = purchasesResult.purchasesList.firstOrNull { it.sku == CStr.PRODUCT_ADS_REMOVE.value}
                    if (purchase?.isAcknowledged == true) {
                        Global.prefs.adsRemovalPurchased = true
                        finish()
                    }
                } else {
                    showGeneralError()
                }
            }

            override fun onBillingServiceDisconnected() {
                /*DO NOTHING*/
            }
        })
    }

    fun getAvailableProducts() {
        if (billingClient.isReady) {
            val params = SkuDetailsParams
                    .newBuilder()
                    .setSkusList(skuList)
                    .setType(BillingClient.SkuType.INAPP)
                    .build()
            billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList ->
                if (responseCode.responseCode == BillingClient.BillingResponseCode.OK) {
                    skuDetails = skuDetailsList.firstOrNull()
                    skuDetails?.let {
                        purchaseButton.text = String.format("BUY %s", it.price)
                        showSuccessOrError(success = true)
                    } ?: run {
                        showSuccessOrError(success = false)
                    }
                } else {
                    showGeneralError()
                }
            }
        } else {
            showGeneralError()
        }
    }

    override fun onPurchasesUpdated(billingResult: BillingResult?, purchases: MutableList<Purchase>?) {
        if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            val purchase = purchases.firstOrNull { it.sku == CStr.PRODUCT_ADS_REMOVE.value}
            if (purchase?.purchaseState == Purchase.PurchaseState.PURCHASED) {
                if (!purchase.isAcknowledged) {
                    val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                            .setPurchaseToken(purchase.purchaseToken)
                            .build()
                    billingClient.acknowledgePurchase(acknowledgePurchaseParams, this@GoPremiumActivity)
                }
            }
        } else if (billingResult?.responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
            Global.prefs.adsRemovalPurchased = true
            finish()
        } else {
            Global.prefs.adsRemovalPurchased = false
            showSuccessOrError(success = true)
        }
    }

    override fun onAcknowledgePurchaseResponse(billingResult: BillingResult?) {
        if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) {
            showThankYouDialog(this@GoPremiumActivity)
            Global.prefs.adsRemovalPurchased = true
        }
    }

    private fun showSuccessOrError(success: Boolean) {
        purchaseProgressBar.visibility = View.GONE
        if (success) {
            purchaseButton.visibility = View.VISIBLE
        } else {
            purchaseUnavailable.visibility = View.VISIBLE
        }
    }

    private fun showGeneralError() {
        purchaseProgressBar.visibility = View.GONE
        purchaseUnavailable.visibility = View.VISIBLE
    }

    companion object {
        fun newIntent(context: Context): Intent {
            return Intent(context, GoPremiumActivity::class.java)
        }
    }

    override fun onSupportNavigateUp(): Boolean {
        finish()
        return true
    }

    public override fun onDestroy() {
        super.onDestroy()
    }

    override fun onPause() {
        super.onPause()
        if (isFinishing) {
            finish()
        }
    }

    private fun showThankYouDialog(context: Context) {
        //Show dialog
    }
}

I can remove it if you specifically want it in Java.