Java
Android
AndroidStudio
GooglePlay

Androidのアプリ内購入を簡単実装

はじめに

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:1.0'
    }

MainActivityを以下のように変更する。

MainActivity.java
  package xx.xx.xx.xx.billingsample;

  import android.support.v7.app.AppCompatActivity;
  import android.os.Bundle;
  import android.util.Log;
  import android.view.View;
  import android.widget.Button;
  import android.widget.TextView;

  import com.android.billingclient.api.BillingClient;
  import com.android.billingclient.api.BillingClientStateListener;
  import com.android.billingclient.api.BillingFlowParams;
  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 java.util.ArrayList;
  import java.util.List;

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

      private final String TAG = "TESTAPP";
      TextView textView1=null;
      BillingClient billingClient;

      // アプリ開始時に呼ばれる
      @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);

          // BillingClientを準備する
          billingClient = BillingClient.newBuilder(this).setListener(this).build();
          billingClient.startConnection(this);
      }

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

                  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(int responseCode, List skuDetailsList) {
                          // Process the result.
                          StringBuffer resultStr = new StringBuffer("");
                          if (responseCode == BillingClient.BillingResponse.OK
                                  && 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");
                              }
                              textView1.setText(resultStr);
                          }
                      }
                  });
      }

      // 購入処理を開始する
      void startPurchase(String sku) {
          BillingFlowParams params = BillingFlowParams.newBuilder()
                  .setSku(sku)
                  .setType(BillingClient.SkuType.INAPP)
                  .build();
          billingClient.launchBillingFlow(this, params);
      }

      // 購入済みアイテムを問い合わせる
      void queryOwned(){
          Purchase.PurchasesResult result =
                  billingClient.queryPurchases(BillingClient.SkuType.INAPP);
          StringBuffer resultStr = new StringBuffer("");
          if(result.getResponseCode ()==0){
              resultStr.append("Query Success\n");

              List<Purchase> purchases = result.getPurchasesList();
              if(purchases.isEmpty()){
                  resultStr.append("Owned Nothing");
              }
              else {
                  for (Purchase purchase : purchases) {
                      resultStr.append(purchase.getSku().toString() + "\n");
                  }
              }
              textView1.setText(resultStr);
          }
      }

      // セットアップの終了時に呼ばれる
      @Override
      public void onBillingSetupFinished(int resultCode) {
          if (resultCode == BillingClient.BillingResponse.OK) {
              textView1.setText("Setup success");
          } else {
              textView1.setText("Setup failed");
          }
      }

      // 切断時に呼ばれる
      @Override
      public void onBillingServiceDisconnected() {
          textView1.setText("Billing service disconnected");
      }

      // 購入結果の更新時に呼ばれる
      @Override
      public void onPurchasesUpdated(int responseCode, List<Purchase> purchases) {
          switch (responseCode) {
              case BillingClient.BillingResponse.OK: 
                  textView1.setText("onPurchasesUpdated: OK");
                  break;

              default: 
                  textView1.setText("onPurchasesUpdated: responseCode=" + responseCode);
          }
      }
  } 

レイアウトファイル「activity_main.xml」を以下のように変更する。

activity_main.xml
   <pre class="brush: 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" >
            <Button
                android:id="@+id/button_get_skus"
                android:text="Get Skus"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"></Button>
            <Button
                android:id="@+id/button_query_owned"
                android:text="Query Owned"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"></Button>
            <Button
                android:id="@+id/button_purchase"
                android:text="Purchase"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"></Button>
            <TextView
                android:id="@+id/text_view1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"></TextView>
            </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.apk) is not signed.

Keyを入力する。

エラー表示の右横に表示された「Fix」をクリックしてGooglePlayに登録しているキーを入力する。未登録なら登録する必要がある。キーの入力手順は以下の通り。
1. 「Fix」を押すと、「Project Structure」の「Signing」タブが表示される。
2. 「+」を押して、Keyを追加する。nameは「config」のままで良い。
3. 「Flavors」タブを選択して、「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内で設定する。
  • 当然ながら、ここでは分かりやすい単純な構成である。実際には、クラス分けやエラー対応、問合せのタイミングなどに工夫が必要である。