Help us understand the problem. What is going on with this article?

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>

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」は、ネットワークアクセスでそれらの場合もチェックするはずが、確認できていない。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした