Implemented in-app purchase for Android Google Play Billing Library 2.0

Caution

Since the version of the library has increased from 1.0 to 2.0, I rewrote the contents. Below is the code for the test trial.

I'll show you an example of code that works somehow, but at your own risk. It is a story that involves money, so it is strongly recommended to check the information of the head family. https://developer.android.com/google/play/billing/billing_library_overview?hl=ja

Introduction

Searching for samples of the in-app purchase process on Android, many were old and complex. It's relatively easy to do with the new Google Play Billing Library, so I'll remember it.

This procedure does not require any settings such as "IInAppBillingService.aidl" or "AndroidManifest.xml". You can also test it before uploading the app to Google Play.

procedure

Create a new project for testing.

Here, I selected a simple "Emply Activity" as the Activity and wrote all the code in the Main Activity. The project name is "Billing Sample".

Add billing to the dependencies of build.gradle (Module :).

build.gradle


    dependencies {
		....
		implementation 'com.android.billingclient:billing:2.0.1'
    }

Change MainActivity as follows.

MainActivity.java


  package xx.xx.xx.xx.billingsample;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
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.PurchaseHistoryRecord;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
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 java.util.ArrayList;
import java.util.List;

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

    TextView textView1;
    private BillingClient billingClient;
    List<SkuDetails> mySkuDetailsList;

    //Called when the app starts
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Prepare operation buttons and result output field
        textView1 = findViewById(R.id.text_view1);
        findViewById(R.id.button_get_skus).setOnClickListener(this);
        findViewById(R.id.button_query_owned).setOnClickListener(this);
        findViewById(R.id.button_purchase).setOnClickListener(this);
        findViewById(R.id.button_purchase_history).setOnClickListener(this);

        //Prepare Billing Client
        billingClient = BillingClient.newBuilder(this)
                .setListener(this).enablePendingPurchases().build();
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                int responseCode = billingResult.getResponseCode();
                if (responseCode == BillingClient.BillingResponseCode.OK) {
                    // The BillingClient is ready. You can query purchases here.
                    textView1.setText("Billing Setup OK");
                } else {
                    showResponseCode(responseCode);
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
                textView1.setText("Billing Servise Disconnected. Retry");
            }
        });
    }

    //Called when the button is clicked
    @Override
    public void onClick(View v) {
        if (v != null) {
            switch (v.getId()) {
                case R.id.button_get_skus:
                    querySkuList();
                    break;

                case R.id.button_query_owned:
                    queryOwned();
                    break;

                case R.id.button_purchase:
                    startPurchase("android.test.purchased");
                    break;

                case R.id.button_purchase_history:
                    queryPurchaseHistory();
                    break;
                default:
                    break;
            }
        }
    }

    //Called when the app is closed
    @Override
    protected void onDestroy() {
        billingClient.endConnection();
        super.onDestroy();
    }

    //Inquire about the item you want to buy
    void querySkuList() {
        List skuList = new ArrayList<>();
        skuList.add("android.test.purchased");  // prepared by Google
        skuList.add("android.test.canceled");
        skuList.add("android.test.refunded");
        skuList.add("android.test.item_unavailable");
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
        billingClient.querySkuDetailsAsync(params.build(),
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(BillingResult billingResult,
                                                     List<SkuDetails> skuDetailsList) {
                        // Process the result.
                        StringBuffer resultStr = new StringBuffer("");
                        int responseCode = billingResult.getResponseCode();
                        if (responseCode == BillingClient.BillingResponseCode.OK) {
                            //Keep Sku details for later purchase process
                            mySkuDetailsList = skuDetailsList;
                            //Show list
                            if (skuDetailsList != null) {
                                for (Object item : skuDetailsList) {
                                    SkuDetails skuDetails = (SkuDetails) item;
                                    String sku = skuDetails.getSku();
                                    String price = skuDetails.getPrice();
                                    resultStr.append("Sku=" + sku + " Price=" + price + "\n");
                                }
                            } else {
                                resultStr.append("No Sku");
                            }
                            textView1.setText(resultStr);
                        } else {
                            showResponseCode(responseCode);
                        }
                    }
                });
    }

    //Start the purchase process
    void startPurchase(String sku) {
        SkuDetails skuDetails = getSkuDetails(sku);
        if (skuDetails != null) {
            BillingFlowParams params = BillingFlowParams.newBuilder()
                    .setSkuDetails(skuDetails)
                    .build();
            BillingResult billingResult = billingClient.launchBillingFlow(this, params);
            showResponseCode(billingResult.getResponseCode());
        }
    }

    //Get the details of the specified SKU from within the list
    SkuDetails getSkuDetails(String sku) {
        SkuDetails skuDetails = null;
        if(mySkuDetailsList==null){
            textView1.setText("Exec [Get Skus] first");
        }else {
            for (SkuDetails sd : mySkuDetailsList) {
                if (sd.getSku().equals(sku)) skuDetails = sd;
            }
            if (skuDetails == null) {
                textView1.setText(sku + " is not found");
            }
        }
        return skuDetails;
    }

    //Called when updating purchase results
    @Override
    public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
        StringBuffer resultStr = new StringBuffer("");
        int billingResultCode = billingResult.getResponseCode();
        if (billingResultCode == BillingClient.BillingResponseCode.OK
                && purchases != null) {
            for (Purchase purchase : purchases) {
                //Approve the purchase
                String state = handlePurchase(purchase);
                //Display the purchased Sku string and approval result
                String sku = purchase.getSku();
                resultStr.append(sku).append("\n");
                resultStr.append(" State=").append(state).append("\n");
            }
            textView1.setText(resultStr);
        } else {
            // Handle error codes.
            showResponseCode(billingResultCode);
        }
    }

    //Approve the purchase
    String handlePurchase(Purchase purchase) {
        String stateStr = "error";
        int purchaseState = purchase.getPurchaseState();
        if (purchaseState == Purchase.PurchaseState.PURCHASED) {
            // Grant entitlement to the user.
            stateStr = "purchased";
            // Acknowledge the purchase if it hasn't already been acknowledged.
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                billingClient.acknowledgePurchase(acknowledgePurchaseParams, this);
            }
        }else if(purchaseState == Purchase.PurchaseState.PENDING){
            stateStr = "pending";
        }else if(purchaseState == Purchase.PurchaseState.UNSPECIFIED_STATE){
            stateStr = "unspecified state";
        }
        return stateStr;
    }

    //Purchase approval results returned
    @Override
    public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
        int responseCode = billingResult.getResponseCode();
        if(responseCode != BillingClient.BillingResponseCode.OK) {
            showResponseCode(responseCode);
        }
    }

    //Inquire about purchased items (cash processing)
    void queryOwned(){
        StringBuffer resultStr = new StringBuffer("");
        Purchase.PurchasesResult purchasesResult
                = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
        int responseCode = purchasesResult.getResponseCode ();
        if(responseCode== BillingClient.BillingResponseCode.OK){
            resultStr.append("Query Success\n");
            List<Purchase> purchases = purchasesResult.getPurchasesList();
            if(purchases.isEmpty()){
                resultStr.append("Owned Nothing");
            } else {
                for (Purchase purchase : purchases) {
                    resultStr.append(purchase.getSku()).append("\n");
                }
            }
            textView1.setText(resultStr);
        }else{
            showResponseCode(responseCode);
        }
    }

    //Inquire about purchase history (network access processing)
    void queryPurchaseHistory() {
        billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP,
                new PurchaseHistoryResponseListener() {
                    @Override
                    public void onPurchaseHistoryResponse(BillingResult billingResult,
                                                          List<PurchaseHistoryRecord> purchasesList) {
                        int responseCode = billingResult.getResponseCode();
                        if (responseCode == BillingClient.BillingResponseCode.OK) {
                            if (purchasesList == null || purchasesList.size() == 0) {
                                textView1.setText("No History");
                            } else {
                                for (PurchaseHistoryRecord purchase : purchasesList) {
                                    // Process the result.
                                    textView1.setText("Purchase History="
                                            + purchase.toString() + "\n");
                                }
                            }
                        } else {
                            showResponseCode(responseCode);
                        }
                    }
        });
    }
    //View server response
    void showResponseCode(int responseCode){
        switch(responseCode){
            case BillingClient.BillingResponseCode.OK:
                textView1.setText("OK");break;
            case BillingClient.BillingResponseCode.USER_CANCELED:
                textView1.setText("USER_CANCELED");break;
            case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
                textView1.setText("SERVICE_UNAVAILABLE");break;
            case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
                textView1.setText("BILLING_UNAVAILABLE");break;
            case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
                textView1.setText("ITEM_UNAVAILABLE");break;
            case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
                textView1.setText("DEVELOPER_ERROR");break;
            case BillingClient.BillingResponseCode.ERROR:
                textView1.setText("ERROR");break;
            case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
                textView1.setText("ITEM_ALREADY_OWNED");break;
            case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
                textView1.setText("ITEM_NOT_OWNED");break;
            case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
                textView1.setText("SERVICE_DISCONNECTED");break;
            case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
                textView1.setText("FEATURE_NOT_SUPPORTED");break;
        }
    }
}

Change the layout file "activity_main.xml" as follows.

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <Button
            android:id="@+id/button_get_skus"
            android:text="Get Skus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/button_query_owned"
            android:text="Query Owned"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/button_purchase"
            android:text="Purchase"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/button_purchase_history"
            android:text="Purchase History"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:id="@+id/text_view1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</android.support.constraint.ConstraintLayout>

Change Build Valiant to Release.

Open Build Valiants from the leftmost tab of Android Studio and change Build Valiant from Debug to Release.

When I try to install it on the actual machine, the following error is displayed.

Error: The apk for your currently selected variant (app-release-unsigned.apk) is not signed.

Enter the Key.

Click "Fix" displayed on the right side of the error display and enter the key registered in Google Play. If you have not registered, you need to register. The key input procedure is as follows.

  1. Press "Fix" to display the "Project Structure> Modules> Signing Configs" tab.
  2. Press "+" to add a Key. The name can remain "config".
  3. Enter the key information (storeFile, storePassword, keyAlias, keyPassword) registered in Google Play.
  4. Select the "Default Config" tab, and in "Signing Config", select the name (config) of the added key from the rightmost selection ▼.

Test on the actual machine.

  1. When you start the app, "Setup Success" is displayed.
  2. Click the "Get Skus" button to display information on the four items you inquired about.
  3. Click the "Query Owned" button to display the purchased items. There is none at first.
  4. Click the "Purchase" button to display the dialog for purchasing "android.test.purchased". Click OK to purchase.
  5. Click the "Query Owned" button again to display the item "android.test.purchased" you just purchased.

Caution

Recommended Posts

Implemented in-app purchase for Android Google Play Billing Library 2.0
Customize infoWindow for google maps for Android
[google-oauth] [python] Google APIs Client Library for Python