Androidのアプリ内購入を実装 Google Play Billing Library 2.0

#注意
ライブラリのバージョンが1.0から2.0に上がったので、内容を書き換えました。以下はテスト試行のコードです。

何とか動くコードの例として示しますが、自己責任でお願いします。お金の絡む話ですので、本家の情報を確認することを強く薦めます。
https://developer.android.com/google/play/billing/billing_library_overview?hl=ja

#はじめに
Androidにおけるアプリ内購入処理のサンプルを検索すると、多くが古い複雑な方法であった。
新たなGoogle Play Billing Libraryを使うと、比較的簡単に実現できたので覚えとして記す。

この手順では「IInAppBillingService.aidl」や「AndroidManifest.xml」などの設定は不要である。
また、GooglePlayにアプリをアップロードする前にテストできる。
#手順
###テスト用に新規プロジェクトを作成する。
ここではActivityとして、単純な「Emply Activity」を選択し、MainActivityの中に全てのコードを記述した。プロジェクト名は「BillingSample」とした。
###build.gradle(Module:)のdependenciesに、billingを追加する。
```build.gradle
    dependencies {
		....
		implementation 'com.android.billingclient:billing:2.0.1'
    }
```
###MainActivityを以下のように変更する。
```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;

    // アプリ開始時に呼ばれる
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 操作ボタンと結果出力欄を準備する
        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);

        // BillingClientを準備する
        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");
            }
        });
    }

    // ボタンクリック時に呼ばれる
    @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;
            }
        }
    }

    // アプリ終了時に呼ばれる
    @Override
    protected void onDestroy() {
        billingClient.endConnection();
        super.onDestroy();
    }

    // 購入したいアイテムを問い合わせる
    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) {
                            // 後の購入手続きのためにSkuの詳細を保持
                            mySkuDetailsList = skuDetailsList;
                            // リストを表示
                            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);
                        }
                    }
                });
    }

    // 購入処理を開始する
    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());
        }
    }

    // 指定したSKUの詳細をリスト内から得る
    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;
    }

    // 購入結果の更新時に呼ばれる
    @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) {
                //購入を承認する
                String state = handlePurchase(purchase);
                //購入したSkuの文字列と承認結果を表示する
                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);
        }
    }

    // 購入を承認する
    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;
    }

    // 購入承認の結果が戻る
    @Override
    public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
        int responseCode = billingResult.getResponseCode();
        if(responseCode != BillingClient.BillingResponseCode.OK) {
            showResponseCode(responseCode);
        }
    }

    // 購入済みアイテムを問い合わせる（キャッシュ処理）
    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);
        }
    }

    // 購入履歴を問い合わせる（ネットワークアクセス処理）
    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);
                        }
                    }
        });
    }
    // サーバの応答を表示する
    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;
        }
    }
}
```

###レイアウトファイル「activity_main.xml」を以下のように変更する。
```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>

```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284841/5a1412ff-097e-4e28-aa30-ae84de08f860.png" width=30%>

###Build ValiantをReleaseに変更する。
AndroidStudioの左端タブからBuild Valiantsを開いて、Build ValiantをDebugからReleaseに変更する。

###実機にインストールしようとすると、以下のエラーが表示される。
>Error: The apk for your currently selected variant (app-release-unsigned.apk) is not signed.

###Keyを入力する。
エラー表示の右横に表示された「Fix」をクリックしてGooglePlayに登録しているキーを入力する。未登録なら登録する必要がある。キーの入力手順は以下の通り。
1. 「Fix」を押すと、「Project Structure＞Modules＞Signing Configs」タブが表示される。
2. 「＋」を押して、Keyを追加する。nameは「config」のままで良い。
3.  Google Playに登録しているキーの情報（storeFile, storePassword, keyAlias, keyPassword）を入力する。
3. 「Default Config」タブを選択して、「Signing Config」にて、右端選択▼から、追加したキーの名前（config）を選択する。

###実機でテストする。
1. アプリをスタートすると、「Setup Success」を表示する。
2. 「Get Skus」ボタンをクリックすると、問い合わせた4つのアイテムの情報を表示する。
3. 「Query Owned」 ボタンをクリックすると、購入済みのアイテムを表示する。最初は無しである。
4. 「Purchase」ボタンをクリックすると、「android.test.purchased」を購入するためのダイアログを表示する。OKをクリックで購入する。
5. 再度「Query Owned」 ボタンをクリックすると、今購入したアイテム「android.test.purchased」を表示する。

#注意
* テストは、実機上でRelease版を使って行う必要がある。仮想端末上では失敗する。
* ここで操作している4つのアイテムは、Googleがテスト用に用意したものである。これらはアプリをGooglePlayにアップロードしなくても使うことができる。
* 「android.test.purchased」は購入が成功するアイテムである。実際の支払いは発生しない。購入済みの状態は、約1日が経過するとリセットされる。
* 自分独自の販売アイテムはアプリをGooglePlayにアップロードしてから、GooglePlay内で設定する。
* 当然ながら、これは各機能をテストするための構成である。実際には、クラス分けやエラー対応、問合せのタイミングなどに工夫が必要である。
* 「QUERY OWNED」はデバイス内のキャッシュをチェックする。再インストールや他端末による購入は正しく反映されない。「PURCHASE HISTORY」は、ネットワークアクセスでそれらの場合もチェックするはずが、確認できていない。